from __future__ import annotations import importlib import io import json import sys from pathlib import Path from types import SimpleNamespace import pytest from gyxx_flow.adapters import COOKIE_SKIP_EXIT_CODE from gyxx_flow.modules.content_marketing.data.tools import ( friday_relogin_parallel as weekly_relogin, ) @pytest.fixture def acceptance_environment( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> Path: evidence = tmp_path / "cookie-evidence.jsonl" monkeypatch.setenv("GYXX_WORKFLOW_ACCEPTANCE", "1") monkeypatch.setenv("GYXX_ACCEPTANCE_EVIDENCE_FILE", str(evidence)) return evidence def _forbidden(message: str): def fail(*_args, **_kwargs): pytest.fail(message) return fail def _config_file(tmp_path: Path) -> Path: config = tmp_path / "config.json" config.write_text( json.dumps( { "login": { "url": "https://login.example.test/", "username": "acceptance-user", "headless": False, } } ), encoding="utf-8", ) return config def _import_supply_script(module_name: str): """Import legacy scripts without letting their stream wrapper close pytest IO.""" stdout = sys.stdout stderr = sys.stderr text_wrapper = io.TextIOWrapper def preserve_pytest_stream(buffer, *args, **kwargs): if buffer is getattr(stdout, "buffer", None): return stdout if buffer is getattr(stderr, "buffer", None): return stderr return text_wrapper(buffer, *args, **kwargs) io.TextIOWrapper = preserve_pytest_stream # type: ignore[assignment,misc] try: return importlib.import_module( f"gyxx_flow.modules.supply_chain.orchestrator.scripts.{module_name}" ) finally: io.TextIOWrapper = text_wrapper sys.stdout = stdout sys.stderr = stderr def test_weekly_relogin_skips_the_entire_qr_workflow( acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: monkeypatch.setattr( weekly_relogin.subprocess, "Popen", _forbidden("acceptance mode must not launch relogin subprocesses"), ) monkeypatch.setattr( weekly_relogin, "_send_lark", _forbidden("acceptance mode must not send QR screenshots"), ) assert weekly_relogin.main() == COOKIE_SKIP_EXIT_CODE assert "SKIPPED_COOKIE" in capsys.readouterr().out assert "content.relogin.weekly" in acceptance_environment.read_text( encoding="utf-8" ) @pytest.mark.parametrize( "module_name", [ "batch_rescrape_xiaohongshu", "batch_rescrape_douyin", "batch_rescrape_bilibili", ], ) def test_batch_rescrape_relogin_never_starts_a_qr_process( module_name: str, acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: module = importlib.import_module( f"gyxx_flow.modules.content_marketing.data.tools.{module_name}" ) monkeypatch.setattr( module.subprocess, "call", _forbidden("acceptance mode must not invoke a relogin script"), ) assert module.do_relogin() is False assert "SKIPPED_COOKIE" in capsys.readouterr().out evidence = acceptance_environment.read_text(encoding="utf-8") assert f"content.{module_name}.relogin" in evidence @pytest.mark.parametrize( "module_name", [ "batch_rescrape_xiaohongshu", "batch_rescrape_douyin", "batch_rescrape_bilibili", ], ) def test_batch_rescrape_propagates_cookie_skip_without_cooldown( module_name: str, acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: module = importlib.import_module( f"gyxx_flow.modules.content_marketing.data.tools.{module_name}" ) monkeypatch.setattr( module, "collect_urls", lambda: [ {"id": index, "url": f"https://example.test/{index}", "title": ""} for index in range(1, 4) ], ) monkeypatch.setattr( module.scraper, "scrape_comments", _forbidden_scrape, ) monkeypatch.setattr( module.time, "sleep", _forbidden("acceptance cookie skips must not enter retry cooldown"), ) assert module.main(dry_run=True) == COOKIE_SKIP_EXIT_CODE def _forbidden_scrape(*_args, **_kwargs): raise RuntimeError("cookie session is invalid") @pytest.mark.parametrize( ("module_name", "manager_name", "login_name"), [ ("ProductReplenishment", "JstManager", "login"), ("PurchaseConfirmation", "ERPLoginManager", "login_with_playwright"), ("PurchaseOrderUpdate", "ERPLoginManager", "login_with_playwright"), ], ) def test_supply_acceptance_without_credentials_never_starts_browser_login( module_name: str, manager_name: str, login_name: str, tmp_path: Path, acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("GYXX_SUPPLY_ERP_PASSWORD", raising=False) module = _import_supply_script(module_name) manager_type = getattr(module, manager_name) if module_name == "ProductReplenishment": manager = manager_type(str(_config_file(tmp_path))) else: manager = manager_type(str(_config_file(tmp_path)), cdp_port=None) monkeypatch.setattr( module, "sync_playwright", _forbidden("acceptance mode must not start an embedded browser"), ) login = getattr(manager, login_name) assert login() is False assert manager._acceptance_cookie_skipped is True assert "SKIPPED_COOKIE" in acceptance_environment.read_text(encoding="utf-8") @pytest.mark.parametrize( "module_name", ["PurchaseConfirmation"], ) def test_supply_missing_credentials_do_not_navigate_or_fill_login( module_name: str, tmp_path: Path, acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("GYXX_SUPPLY_ERP_PASSWORD", raising=False) module = _import_supply_script(module_name) manager = module.ERPLoginManager(str(_config_file(tmp_path)), cdp_port=22555) page = SimpleNamespace(context=SimpleNamespace(cookies=lambda: [])) navigation_flags: list[bool] = [] class PlaywrightStarter: def start(self): return object() def prepare(_url, *, navigate=True): navigation_flags.append(navigate) return page monkeypatch.setattr(module, "sync_playwright", PlaywrightStarter) monkeypatch.setattr(manager, "_prepare_login_page", prepare) monkeypatch.setattr( manager, "_fill_first_visible", _forbidden("acceptance mode must not fill login credentials"), ) assert manager.login_with_playwright() is False assert manager._acceptance_cookie_skipped is True assert navigation_flags == [True] def test_purchase_confirmation_uses_credential_fallback_but_skips_captcha( tmp_path: Path, acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: module = _import_supply_script("PurchaseConfirmation") monkeypatch.setenv("GYXX_SUPPLY_ERP_PASSWORD", "configured-for-test") manager = module.ERPLoginManager(str(_config_file(tmp_path)), cdp_port=None) navigation_flags: list[bool] = [] filled: list[str] = [] class Locator: def __init__(self, selector: str): self.selector = selector @property def first(self): return self def count(self): return 1 if "captcha" in self.selector else 0 def is_visible(self): return True class Page: context = SimpleNamespace(cookies=lambda: []) keyboard = SimpleNamespace(press=lambda _key: None) def goto(self, *_args, **_kwargs): return None def locator(self, selector): return Locator(selector) class PlaywrightStarter: def start(self): return object() page = Page() def prepare(_url, *, navigate=True): navigation_flags.append(navigate) return page monkeypatch.setattr(module, "sync_playwright", PlaywrightStarter) monkeypatch.setattr(manager, "_prepare_login_page", prepare) monkeypatch.setattr( manager, "_fill_first_visible", lambda _page, _selectors, _value, label, **_kwargs: filled.append(label) or True, ) monkeypatch.setattr(manager, "_dismiss_post_login_dialogs", lambda *_a, **_k: None) monkeypatch.setattr(module.time, "sleep", lambda _seconds: None) assert manager.login_with_playwright() is False assert manager._acceptance_cookie_skipped is True assert navigation_flags == [True] assert filled == ["username", "password"] assert "captcha or QR" in acceptance_environment.read_text(encoding="utf-8") def test_product_replenishment_invalid_cdp_cookie_never_falls_back_to_login( tmp_path: Path, acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: module = _import_supply_script("ProductReplenishment") manager = module.JstManager(str(_config_file(tmp_path))) manager._page = object() monkeypatch.setattr(manager, "connect_existing_browser", lambda _url: True) monkeypatch.setattr(manager, "_page_has_inventory_access", lambda _page: False) monkeypatch.setattr( manager, "_login_on_current_page", _forbidden("acceptance mode must not run the fresh-login fallback"), ) assert manager.login( use_existing_browser=True, cdp_url="http://127.0.0.1:22556", ) is False assert manager._acceptance_cookie_skipped is True def test_product_replenishment_production_existing_browser_flow_is_preserved( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("GYXX_WORKFLOW_ACCEPTANCE", raising=False) monkeypatch.setenv("GYXX_SUPPLY_ERP_PASSWORD", "test-only-password") module = _import_supply_script("ProductReplenishment") manager = module.JstManager(str(_config_file(tmp_path))) manager._page = object() called: list[tuple[str, str, str]] = [] monkeypatch.setattr(manager, "connect_existing_browser", lambda _url: True) monkeypatch.setattr( manager, "_login_on_current_page", lambda url, username, credential: called.append( (url, username, credential) ) or True, ) assert manager.login( use_existing_browser=True, cdp_url="http://127.0.0.1:22556", ) is True assert called == [ ("https://login.example.test/", "acceptance-user", "test-only-password") ] def test_product_replenishment_force_login_clears_dedicated_browser_session( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("GYXX_SUPPLY_FORCE_ERP_LOGIN", "1") monkeypatch.setenv("GYXX_SUPPLY_ERP_PASSWORD", "test-only-password") module = _import_supply_script("ProductReplenishment") manager = module.JstManager(str(_config_file(tmp_path))) manager._page = object() class Context: cleared = False def clear_cookies(self) -> None: self.cleared = True context = Context() manager._context = context monkeypatch.setattr(manager, "connect_existing_browser", lambda _url: True) monkeypatch.setattr( manager, "_page_has_inventory_access", _forbidden("forced login must not accept the existing ERP session"), ) monkeypatch.setattr(manager, "_login_on_current_page", lambda *_args: True) assert manager.login(True, "http://127.0.0.1:22556") is True assert context.cleared is True def test_product_replenishment_main_returns_cookie_skip_code_without_cdp( tmp_path: Path, acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: module = _import_supply_script("ProductReplenishment") class Manager: cdp_url = None _acceptance_cookie_skipped = False login_calls: list[bool] = [] def __init__(self, _config_path): pass def _skip_acceptance_cookie(self, _reason): self._acceptance_cookie_skipped = True return False def login(self, *, use_existing_browser=False, cdp_url=None): del cdp_url self.login_calls.append(use_existing_browser) return self._skip_acceptance_cookie("interactive challenge") def close(self, *, disconnect=False): del disconnect monkeypatch.setattr(module, "JstManager", Manager) monkeypatch.setattr(module, "REPLENISHMENT_RAW_ROOT", tmp_path / "raw") monkeypatch.setattr(module, "REPLENISHMENT_WORK_ROOT", tmp_path / "work") monkeypatch.setattr(module, "REPLENISHMENT_EXPORT_ROOT", tmp_path / "export") assert module.main(cdp_url=None) == COOKIE_SKIP_EXIT_CODE assert Manager.login_calls == [False] def test_purchase_confirmation_main_exits_with_cookie_skip_code( acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: module = _import_supply_script("PurchaseConfirmation") closed: list[bool] = [] class Manager: _acceptance_cookie_skipped = True def __init__(self, _config_path, cdp_port=None): pass def login_with_playwright(self): return False def close_browser(self): closed.append(True) monkeypatch.setattr(module, "PurchaseManager", Manager) monkeypatch.setattr(module, "_persist_run_to_pg", lambda _result: None) monkeypatch.setattr(module.sys, "platform", "linux") with pytest.raises(SystemExit) as raised: module.main(cdp_port=22557) assert raised.value.code == COOKIE_SKIP_EXIT_CODE assert closed == [True] def test_purchase_order_update_main_exits_with_cookie_skip_code( tmp_path: Path, acceptance_environment: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: module = _import_supply_script("PurchaseOrderUpdate") config = _config_file(tmp_path) task = tmp_path / "task.json" task.write_text( json.dumps( { "sku_list": ["10426004"], "target_date": "2026-08-02", "remark": "acceptance fixture", } ), encoding="utf-8", ) closed: list[bool] = [] class Manager: _acceptance_cookie_skipped = True def __init__(self, _config_path, cdp_port=None): pass def login_with_playwright(self): return False def close_browser(self): closed.append(True) monkeypatch.setattr(module, "CONFIG_PATH", config) monkeypatch.setattr(module, "PurchaseManager", Manager) with pytest.raises(SystemExit) as raised: module.main(task_file=task, cdp_port=22558) assert raised.value.code == COOKIE_SKIP_EXIT_CODE assert closed == [True]