feat: complete production workflow migration

This commit is contained in:
2026-08-06 14:29:57 +08:00
parent 7f215e79c4
commit 8df5266abb
448 changed files with 56937 additions and 14619 deletions
+493
View File
@@ -0,0 +1,493 @@
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]