from __future__ import annotations import json import multiprocessing import os import sqlite3 import threading from dataclasses import replace from pathlib import Path import pytest import gyxx_flow.adapters.acceptance_policy as acceptance_policy_module from gyxx_flow.adapters import ( WORKFLOW_ACCEPTANCE_RECIPIENT_OPEN_ID, RuntimeIntegrationBinding, WorkflowAcceptancePolicy, WorkflowAcceptancePolicyError, ) def _write_acceptance_evidence(path: str, worker: int, count: int) -> None: environment = { "GYXX_WORKFLOW_ACCEPTANCE": "1", "GYXX_ACCEPTANCE_EVIDENCE_FILE": path, "GYXX_WORKFLOW_ID": "content.metrics.daily", "GYXX_RUN_ID": f"worker-{worker}", } os.environ.update(environment) policy = WorkflowAcceptancePolicy.from_environment(environment) for index in range(count): policy.record( "feishu_write_skipped", operation="concurrency-test", details={"worker": worker, "index": index}, ) def _binding(tmp_path: Path) -> RuntimeIntegrationBinding: return RuntimeIntegrationBinding( script_id="content_marketing:collector.py", module="content_marketing", entry="collector.py", cdp_port=22998, cdp_url="http://127.0.0.1:22998", profile_dir=tmp_path / "profile", cookie_file=tmp_path / "cookies.json", storage_state_file=tmp_path / "storage_state.json", ) def _environment(tmp_path: Path) -> dict[str, str]: return { "GYXX_WORKFLOW_ACCEPTANCE": "1", "GYXX_DATA_ROOT": str(tmp_path), "GYXX_WORKFLOW_ID": "content.metrics.daily", "GYXX_RUN_ID": "acceptance-run-1", } def test_disabled_policy_does_not_change_recipients_or_writes(tmp_path: Path) -> None: policy = WorkflowAcceptancePolicy.from_environment({}) assert policy.notification_recipients(("ou_a", "ou_a", "ou_b")) == ( "ou_a", "ou_b", ) assert policy.skip_feishu_write("base.upsert") is False assert policy.evidence_file is None def test_acceptance_policy_is_fail_closed_and_forces_wang_yunlong( tmp_path: Path, ) -> None: policy = WorkflowAcceptancePolicy.from_environment(_environment(tmp_path)) assert policy.notification_recipients(("ou_other", "oc_group")) == ( WORKFLOW_ACCEPTANCE_RECIPIENT_OPEN_ID, ) assert policy.environment()["GYXX_FEISHU_TABLE_WRITE_DISABLED"] == "1" assert policy.environment()["GYXX_COOKIE_INVALID_SKIP"] == "1" with pytest.raises(WorkflowAcceptancePolicyError, match="Wang Yunlong"): WorkflowAcceptancePolicy.from_environment( { **_environment(tmp_path), "GYXX_NOTIFICATION_RECIPIENT_OPEN_ID": "ou_other", } ) with pytest.raises(WorkflowAcceptancePolicyError, match="writes"): WorkflowAcceptancePolicy.from_environment( { **_environment(tmp_path), "GYXX_FEISHU_TABLE_WRITE_DISABLED": "0", } ) def test_feishu_write_skip_appends_run_scoped_evidence( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: environment = _environment(tmp_path) policy = WorkflowAcceptancePolicy.from_environment(environment) monkeypatch.setenv("GYXX_WORKFLOW_ID", environment["GYXX_WORKFLOW_ID"]) monkeypatch.setenv("GYXX_RUN_ID", environment["GYXX_RUN_ID"]) assert policy.skip_feishu_write( "bitable.record-upsert", details={"table": "tbl1", "api_token": "must-not-leak"}, ) assert policy.evidence_file is not None payload = json.loads(policy.evidence_file.read_text(encoding="utf-8")) assert payload["event"] == "feishu_write_skipped" assert payload["workflow_id"] == "content.metrics.daily" assert payload["run_id"] == "acceptance-run-1" assert payload["details"]["api_token"] == "" def test_acceptance_evidence_recursively_redacts_nested_secrets( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: environment = _environment(tmp_path) policy = WorkflowAcceptancePolicy.from_environment(environment) monkeypatch.setenv("GYXX_WORKFLOW_ID", environment["GYXX_WORKFLOW_ID"]) monkeypatch.setenv("GYXX_RUN_ID", environment["GYXX_RUN_ID"]) credential_values = {"value": "not-a-real-" + "nested-secret-value"} details = { "request": { "headers": {"access_" + "token": credential_values["value"]}, "items": [ {"pass" + "word": credential_values["value"]}, ("safe", {"secret_" + "key": credential_values["value"]}), ], }, "safe_value": "visible", } safe_details = acceptance_policy_module._safe_details(details) assert isinstance(safe_details["request"]["items"][1], tuple) policy.record( "nested-redaction-test", details=details, ) assert policy.evidence_file is not None text = policy.evidence_file.read_text(encoding="utf-8") assert credential_values["value"] not in text payload = json.loads(text) assert payload["details"] == { "request": { "headers": {"access_token": ""}, "items": [ {"password": ""}, ["safe", {"secret_key": ""}], ], }, "safe_value": "visible", } def test_acceptance_evidence_is_valid_under_multiprocess_writers( tmp_path: Path, ) -> None: evidence_file = tmp_path / "concurrent-evidence.jsonl" context = multiprocessing.get_context("spawn") workers = [ context.Process( target=_write_acceptance_evidence, args=(str(evidence_file), worker, 25), ) for worker in range(4) ] for process in workers: process.start() for process in workers: process.join(timeout=30) assert [process.exitcode for process in workers] == [0, 0, 0, 0] lines = evidence_file.read_text(encoding="utf-8").splitlines() payloads = [json.loads(line) for line in lines] assert len(payloads) == 100 assert { (payload["details"]["worker"], payload["details"]["index"]) for payload in payloads } == {(worker, index) for worker in range(4) for index in range(25)} @pytest.mark.skipif(not hasattr(os, "fork"), reason="requires POSIX fork") def test_acceptance_evidence_resets_inherited_thread_lock_after_fork( tmp_path: Path, ) -> None: evidence_file = tmp_path / "fork-evidence.jsonl" locked = threading.Event() release = threading.Event() def hold_parent_lock() -> None: with acceptance_policy_module._EVIDENCE_THREAD_LOCK: locked.set() release.wait(timeout=10) thread = threading.Thread(target=hold_parent_lock) thread.start() assert locked.wait(timeout=2) context = multiprocessing.get_context("fork") process = context.Process( target=_write_acceptance_evidence, args=(str(evidence_file), 1, 1), ) try: process.start() process.join(timeout=5) finally: release.set() thread.join(timeout=2) if process.is_alive(): process.terminate() process.join(timeout=2) assert process.exitcode == 0 payload = json.loads(evidence_file.read_text(encoding="utf-8")) assert payload["run_id"] == "worker-1" def test_acceptance_evidence_thread_lock_has_a_timeout( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: policy = WorkflowAcceptancePolicy.from_environment(_environment(tmp_path)) monkeypatch.setattr( acceptance_policy_module, "_EVIDENCE_LOCK_TIMEOUT_SECONDS", 0.05, ) lock = acceptance_policy_module._EVIDENCE_THREAD_LOCK assert lock.acquire(timeout=1) try: with pytest.raises(WorkflowAcceptancePolicyError, match="this process"): policy.record("lock-timeout-test") finally: lock.release() def test_cookie_preflight_skips_missing_invalid_and_expired_state( tmp_path: Path, ) -> None: policy = WorkflowAcceptancePolicy.from_environment(_environment(tmp_path)) binding = _binding(tmp_path) missing = policy.preflight_cookie(binding, now_epoch=100.0) assert missing.status == "SKIPPED_COOKIE" assert missing.reason == "browser state is missing" binding.cookie_file.write_text("not-json", encoding="utf-8") invalid = policy.preflight_cookie(binding, now_epoch=100.0) assert invalid.status == "SKIPPED_COOKIE" assert invalid.reason == "cookie file is unreadable" binding.cookie_file.write_text( json.dumps([{"name": "session", "value": "x", "expires": 99.0}]), encoding="utf-8", ) expired = policy.preflight_cookie(binding, now_epoch=100.0) assert expired.status == "SKIPPED_COOKIE" assert expired.reason == "all cookies are expired" def test_cookie_preflight_accepts_unexpired_or_profile_cookie_state( tmp_path: Path, ) -> None: policy = WorkflowAcceptancePolicy.from_environment(_environment(tmp_path)) binding = _binding(tmp_path) binding.cookie_file.write_text( json.dumps([{"name": "session", "value": "x", "expires": 101.0}]), encoding="utf-8", ) assert policy.preflight_cookie(binding, now_epoch=100.0).status == "READY" binding.cookie_file.unlink() binding.profile_dir.mkdir() (binding.profile_dir / "Local State").write_text("{}", encoding="utf-8") assert policy.preflight_cookie(binding, now_epoch=100.0).status == "SKIPPED_COOKIE" cookie_db = binding.profile_dir / "Default" / "Network" / "Cookies" cookie_db.parent.mkdir(parents=True) with sqlite3.connect(cookie_db) as connection: connection.execute("CREATE TABLE cookies (expires_utc INTEGER NOT NULL)") connection.execute("INSERT INTO cookies VALUES (?)", (11_644_473_701_000_000,)) assert policy.preflight_cookie(binding, now_epoch=100.0).status == "READY" @pytest.mark.parametrize( "payload", [ [{"name": "session", "value": "x", "domain": ".example.test"}], { "cookies": [ {"name": "session", "value": "x", "domain": ".example.test"} ] }, { "cookies": [ {"name": "session", "value": "x", "domain": ".example.test"} ], "origins": [{"origin": "https://example.test", "localStorage": []}], }, ], ) def test_cookie_preflight_supports_cookie_bundles_and_storage_state( tmp_path: Path, payload: object, ) -> None: policy = WorkflowAcceptancePolicy.from_environment(_environment(tmp_path)) binding = replace( _binding(tmp_path), required_cookie_domains=("example.test",), required_cookie_names=("session",), ) binding.cookie_file.write_text(json.dumps(payload), encoding="utf-8") assert policy.preflight_cookie(binding, now_epoch=100.0).status == "READY" def test_cookie_preflight_rejects_an_unrelated_domain_or_cookie_name( tmp_path: Path, ) -> None: policy = WorkflowAcceptancePolicy.from_environment(_environment(tmp_path)) binding = replace( _binding(tmp_path), required_cookie_domains=("target.example",), required_cookie_names=("target_session",), ) binding.cookie_file.write_text( json.dumps( [ { "name": "unrelated_session", "value": "x", "domain": ".other.example", } ] ), encoding="utf-8", ) wrong_domain = policy.preflight_cookie(binding, now_epoch=100.0) assert wrong_domain.status == "SKIPPED_COOKIE" assert "platform domain" in wrong_domain.reason binding.cookie_file.write_text( json.dumps( [ { "name": "unrelated_session", "value": "x", "domain": ".target.example", } ] ), encoding="utf-8", ) wrong_name = policy.preflight_cookie(binding, now_epoch=100.0) assert wrong_name.status == "SKIPPED_COOKIE" assert "login cookie" in wrong_name.reason @pytest.mark.parametrize("login_mode", ["B", "C"]) def test_credential_capable_modes_do_not_require_cookie_when_credentials_exist( tmp_path: Path, login_mode: str, ) -> None: policy = WorkflowAcceptancePolicy.from_environment(_environment(tmp_path)) account_name = "TARGET_" + "ACCOUNT" password_name = "TARGET_" + "PASSWORD" configured_value = "config" + "ured" binding = replace( _binding(tmp_path), login_mode=login_mode, required_cookie_domains=("target.example",), credential_env_names=(account_name, password_name), ) missing = policy.preflight_cookie(binding, now_epoch=100.0, environment={}) assert missing.status == "SKIPPED_COOKIE" available = policy.preflight_cookie( binding, now_epoch=100.0, environment={ account_name: configured_value, password_name: configured_value, }, ) assert available.status == "READY" assert available.reason == "credential login fallback is available" def test_interactive_login_mode_is_always_skipped_during_acceptance( tmp_path: Path, ) -> None: policy = WorkflowAcceptancePolicy.from_environment(_environment(tmp_path)) binding = replace(_binding(tmp_path), login_mode="D") binding.cookie_file.write_text( json.dumps([{"name": "session", "value": "x"}]), encoding="utf-8", ) result = policy.preflight_cookie(binding, now_epoch=100.0) assert result.status == "SKIPPED_COOKIE" assert "interactive login" in result.reason