| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- import json
- from datetime import datetime
- from pathlib import Path
- from ops.scan import ScanResult
- def _write_token(path: Path, *, email: str, created_at: str) -> None:
- path.write_text(
- (
- "{\n"
- f' "email": "{email}",\n'
- ' "access_token": "tok",\n'
- ' "account_id": "acct",\n'
- f' "created_at": "{created_at}"\n'
- "}\n"
- ),
- encoding="utf-8",
- )
- def _write_token_with_provenance(path: Path, *, email: str, created_at: str) -> None:
- path.write_text(
- (
- "{\n"
- f' "email": "{email}",\n'
- ' "access_token": "tok",\n'
- ' "account_id": "acct",\n'
- f' "created_at": "{created_at}",\n'
- ' "registration_fingerprint_profile": "chrome120_win",\n'
- ' "registration_proxy_key": "台湾原生-01",\n'
- ' "registration_proxy_region": "tw",\n'
- ' "registration_post_create_gate": "add_phone"\n'
- "}\n"
- ),
- encoding="utf-8",
- )
- def test_responses_survival_seeds_recent_cohort_and_records_first_401(monkeypatch, tmp_path: Path) -> None:
- from ops.responses_survival import responses_survival_once
- newer = tmp_path / "newer@example.com.json"
- older = tmp_path / "older@example.com.json"
- _write_token(newer, email="newer@example.com", created_at="2026-03-30T20:20:00+08:00")
- _write_token(older, email="older@example.com", created_at="2026-03-30T20:19:00+08:00")
- state_file = tmp_path / "responses_survival.json"
- def fake_probe(path, proxy, timeout): # type: ignore[no-untyped-def]
- del proxy, timeout
- if path.name == newer.name:
- return ScanResult(file=path.name, category="normal", status_code=200, detail="responses_ok")
- return ScanResult(file=path.name, category="invalid", status_code=401, detail="unauthorized")
- monkeypatch.setattr("ops.responses_survival.probe_responses_token_file", fake_probe)
- result = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=2,
- proxy=None,
- timeout_seconds=30,
- reseed=True,
- )
- assert result["probe_mode"] == "responses"
- assert result["seeded"] is True
- assert [member["email"] for member in result["members"]] == ["newer@example.com", "older@example.com"]
- assert result["summary"]["tracked"] == 2
- assert result["summary"]["alive"] == 1
- assert result["summary"]["invalid"] == 1
- invalid_member = next(member for member in result["members"] if member["email"] == "older@example.com")
- assert invalid_member["first_invalid_at"]
- assert invalid_member["survival_seconds"] is not None
- assert invalid_member["survival_seconds"] >= 0
- assert {item["to"] for item in result["changes"]} == {"normal", "invalid"}
- def test_responses_survival_preserves_401_semantics_after_pool_file_is_removed(
- monkeypatch, tmp_path: Path
- ) -> None:
- from ops.responses_survival import responses_survival_once
- tracked = tmp_path / "tracked@example.com.json"
- _write_token(tracked, email="tracked@example.com", created_at="2026-03-31T12:00:00+08:00")
- state_file = tmp_path / "responses_survival.json"
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda path, proxy, timeout: ScanResult(file=path.name, category="invalid", status_code=401, detail="no_organization"),
- )
- first = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=True,
- )
- first_member = first["members"][0]
- assert first_member["last_probe_category"] == "invalid"
- assert first_member["state"] == "invalid"
- assert first["summary"]["invalid"] == 1
- assert first["summary"]["missing"] == 0
- tracked.unlink()
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda path, proxy, timeout: ScanResult(file=path.name, category="missing", status_code=None, detail=f"missing_file: {path}"),
- )
- second = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=False,
- )
- member = second["members"][0]
- assert member["last_probe_category"] == "invalid"
- assert member["state"] == "invalid_removed"
- assert member["first_invalid_at"] == first_member["first_invalid_at"]
- assert member["missing_at"]
- assert second["summary"]["invalid"] == 1
- assert second["summary"]["missing"] == 0
- assert second["summary"]["removed_after_invalid"] == 1
- assert second["changes"][-1]["from"] == "invalid"
- assert second["changes"][-1]["to"] == "invalid_removed"
- def test_responses_survival_keeps_first_401_terminal_after_later_transport_error(
- monkeypatch, tmp_path: Path
- ) -> None:
- from ops.responses_survival import responses_survival_once
- tracked = tmp_path / "tracked@example.com.json"
- _write_token(tracked, email="tracked@example.com", created_at="2026-03-31T12:00:00+08:00")
- state_file = tmp_path / "responses_survival.json"
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda path, proxy, timeout: ScanResult(file=path.name, category="invalid", status_code=401, detail="no_organization"),
- )
- responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=True,
- )
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda path, proxy, timeout: ScanResult(file=path.name, category="transport_error", status_code=None, detail="tls error"),
- )
- second = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=False,
- )
- member = second["members"][0]
- assert member["last_probe_category"] == "invalid"
- assert member["state"] == "invalid"
- assert second["summary"]["invalid"] == 1
- assert second["summary"]["transport_error"] == 0
- def test_responses_survival_records_registration_provenance_and_first_use_metadata(
- monkeypatch,
- tmp_path: Path,
- ) -> None:
- from ops.responses_survival import responses_survival_once
- tracked = tmp_path / "tracked@example.com.json"
- _write_token_with_provenance(tracked, email="tracked@example.com", created_at="2026-03-31T12:00:00+08:00")
- state_file = tmp_path / "responses_survival.json"
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda path, proxy, timeout: ScanResult(file=path.name, category="normal", status_code=200, detail="responses_ok"),
- )
- result = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy="http://127.0.0.1:7899",
- timeout_seconds=30,
- reseed=True,
- )
- member = result["members"][0]
- assert member["registration_fingerprint_profile"] == "chrome120_win"
- assert member["registration_proxy_key"] == "台湾原生-01"
- assert member["registration_proxy_region"] == "tw"
- assert member["registration_post_create_gate"] == "add_phone"
- assert member["first_use_at"]
- assert member["first_use_age_seconds"] is not None
- assert member["first_use_fingerprint_profile"] == "chrome120_win"
- assert member["fingerprint_consistent"] is True
- def test_responses_survival_prefers_recent_provenance_seed(monkeypatch, tmp_path: Path) -> None:
- from ops.responses_survival import responses_survival_once
- latest_without_provenance = tmp_path / "latest@example.com.json"
- recent_with_provenance = tmp_path / "recent@example.com.json"
- _write_token(latest_without_provenance, email="latest@example.com", created_at="2026-03-31T14:04:00+08:00")
- _write_token_with_provenance(recent_with_provenance, email="recent@example.com", created_at="2026-03-31T14:03:30+08:00")
- state_file = tmp_path / "responses_survival.json"
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda path, proxy, timeout: ScanResult(file=path.name, category="normal", status_code=200, detail=str(proxy or "")),
- )
- result = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=True,
- )
- assert [member["email"] for member in result["members"]] == ["recent@example.com"]
- def test_responses_survival_reuses_registration_proxy_affinity(monkeypatch, tmp_path: Path) -> None:
- from ops.responses_survival import responses_survival_once
- tracked = tmp_path / "tracked@example.com.json"
- _write_token_with_provenance(tracked, email="tracked@example.com", created_at="2026-03-31T12:00:00+08:00")
- state_file = tmp_path / "responses_survival.json"
- recorded: dict[str, object] = {}
- class FakeLease:
- name = "台湾原生-01"
- proxy_url = "socks5://127.0.0.1:17891"
- local_port = 17891
- class FakePool:
- def acquire(self, timeout=5.0, preferred_name=None, preferred_regions=()): # type: ignore[no-untyped-def]
- recorded["preferred_name"] = preferred_name
- recorded["preferred_regions"] = tuple(preferred_regions)
- return FakeLease()
- def release(self, lease, *, success, stage=None): # type: ignore[no-untyped-def]
- recorded["released"] = (lease.name, success, stage)
- def fake_probe(path, proxy, timeout): # type: ignore[no-untyped-def]
- recorded["proxy"] = proxy
- return ScanResult(file=path.name, category="normal", status_code=200, detail="responses_ok")
- monkeypatch.setattr("ops.responses_survival.probe_responses_token_file", fake_probe)
- result = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy="http://127.0.0.1:7899",
- timeout_seconds=30,
- reseed=True,
- proxy_pool=FakePool(),
- )
- member = result["members"][0]
- assert recorded["preferred_name"] == "台湾原生-01"
- assert recorded["preferred_regions"] == ("tw",)
- assert recorded["proxy"] == "socks5://127.0.0.1:17891"
- assert member["first_use_proxy_key"] == "台湾原生-01"
- assert member["first_use_proxy_region"] == "tw"
- def test_responses_survival_marks_add_phone_warmup_passed_after_two_successful_probes(
- monkeypatch, tmp_path: Path
- ) -> None:
- from ops.responses_survival import responses_survival_once
- tracked = tmp_path / "tracked@example.com.json"
- _write_token_with_provenance(tracked, email="tracked@example.com", created_at="2026-03-31T12:00:00+08:00")
- state_file = tmp_path / "responses_survival.json"
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda path, proxy, timeout: ScanResult(file=path.name, category="normal", status_code=200, detail="responses_ok"),
- )
- first = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=True,
- warmup_min_age_seconds=0,
- )
- second = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=False,
- warmup_min_age_seconds=0,
- )
- assert first["members"][0]["warmup_state"] == "pending"
- assert second["members"][0]["warmup_state"] == "passed"
- assert second["members"][0]["warmup_passed"] is True
- def test_responses_survival_requires_min_age_before_single_probe_promotion(
- monkeypatch, tmp_path: Path
- ) -> None:
- from ops.responses_survival import responses_survival_once
- pool_dir = tmp_path / "pool"
- pool_dir.mkdir()
- state_file = tmp_path / "responses_survival.json"
- created_at = datetime.now().astimezone().isoformat(timespec="seconds")
- token_path = pool_dir / "fresh@example.com.json"
- token_path.write_text(
- json.dumps(
- {
- "email": "fresh@example.com",
- "access_token": "tok",
- "account_id": "acct",
- "created_at": created_at,
- "registration_post_create_gate": "add_phone",
- "warmup_required": True,
- "cpa_sync_status": "warmup_pending",
- },
- ensure_ascii=False,
- ),
- encoding="utf-8",
- )
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda *_args, **_kwargs: ScanResult(
- file=str(token_path),
- category="normal",
- status_code=200,
- detail="responses_ok",
- ),
- )
- result = responses_survival_once(
- pool_dir=pool_dir,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=10,
- reseed=True,
- warmup_min_age_seconds=90,
- warmup_min_successful_probes=1,
- )
- member = result["members"][0]
- assert member["successful_probe_count"] == 1
- assert member["warmup_state"] == "pending"
- assert member["warmup_passed"] is False
- def test_responses_survival_promotes_passed_warmup_account_to_cpa(monkeypatch, tmp_path: Path) -> None:
- from core.settings import AppSettings
- from ops.responses_survival import responses_survival_once
- tracked = tmp_path / "tracked@example.com.json"
- tracked.write_text(
- (
- "{\n"
- ' "email": "tracked@example.com",\n'
- ' "access_token": "tok",\n'
- ' "account_id": "acct",\n'
- ' "created_at": "2026-03-31T12:00:00+08:00",\n'
- ' "registration_fingerprint_profile": "chrome120_win",\n'
- ' "registration_proxy_key": "台湾原生-01",\n'
- ' "registration_proxy_region": "tw",\n'
- ' "registration_post_create_gate": "add_phone",\n'
- ' "warmup_required": true,\n'
- ' "warmup_state": "pending",\n'
- ' "warmup_passed": false,\n'
- ' "cpa_sync_status": "warmup_pending"\n'
- "}\n"
- ),
- encoding="utf-8",
- )
- state_file = tmp_path / "responses_survival.json"
- uploaded: list[str] = []
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda path, proxy, timeout: ScanResult(file=path.name, category="normal", status_code=200, detail="responses_ok"),
- )
- monkeypatch.setattr("ops.responses_survival.get_management_key", lambda: "secret") # type: ignore[no-untyped-def]
- monkeypatch.setattr(
- "platforms.chatgpt.cpa_upload.upload_to_cpa",
- lambda token_data, api_url=None, api_key=None, proxy=None: uploaded.append(token_data["email"]) or (True, "ok"),
- )
- settings = AppSettings(cpa_management_base_url="http://127.0.0.1:8317/v0/management", backend="cpa")
- responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=True,
- warmup_min_age_seconds=0,
- warmup_min_successful_probes=2,
- settings=settings,
- )
- second = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=False,
- warmup_min_age_seconds=0,
- warmup_min_successful_probes=2,
- settings=settings,
- )
- assert uploaded == ["tracked@example.com"]
- assert second["members"][0]["warmup_state"] == "passed"
- assert second["promotion_stats"]["promoted_success_total"] == 1
- assert second["promotion_stats"]["promoted_failure_total"] == 0
- persisted = json.loads(tracked.read_text(encoding="utf-8"))
- assert persisted["cpa_sync_status"] == "synced"
- third = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=False,
- warmup_min_age_seconds=0,
- warmup_min_successful_probes=2,
- settings=settings,
- )
- assert third["promotion_stats"]["promoted_success_total"] == 1
- assert third["promotion_stats"]["promoted_failure_total"] == 0
- def test_responses_survival_auto_enrolls_new_warmup_member_into_existing_cohort(
- monkeypatch, tmp_path: Path
- ) -> None:
- from ops.responses_survival import responses_survival_once
- stable = tmp_path / "stable@example.com.json"
- _write_token(stable, email="stable@example.com", created_at="2026-03-31T18:00:00+08:00")
- state_file = tmp_path / "responses_survival.json"
- monkeypatch.setattr(
- "ops.responses_survival.probe_responses_token_file",
- lambda path, proxy, timeout: ScanResult(file=path.name, category="normal", status_code=200, detail="responses_ok"),
- )
- first = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=True,
- )
- assert [member["email"] for member in first["members"]] == ["stable@example.com"]
- pending = tmp_path / "pending@example.com.json"
- pending.write_text(
- (
- "{\n"
- ' "email": "pending@example.com",\n'
- ' "access_token": "tok",\n'
- ' "account_id": "acct",\n'
- ' "created_at": "2026-03-31T18:30:00+08:00",\n'
- ' "registration_fingerprint_profile": "chrome120_win",\n'
- ' "registration_proxy_key": "台湾原生-01",\n'
- ' "registration_proxy_region": "tw",\n'
- ' "registration_post_create_gate": "add_phone",\n'
- ' "warmup_required": true,\n'
- ' "warmup_state": "pending",\n'
- ' "warmup_passed": false,\n'
- ' "cpa_sync_status": "warmup_pending"\n'
- "}\n"
- ),
- encoding="utf-8",
- )
- second = responses_survival_once(
- pool_dir=tmp_path,
- state_file=state_file,
- cohort_size=1,
- proxy=None,
- timeout_seconds=30,
- reseed=False,
- warmup_min_age_seconds=999999,
- warmup_min_successful_probes=2,
- )
- assert [member["email"] for member in second["members"]] == ["pending@example.com"]
- assert second["members"][0]["warmup_state"] == "pending"
- assert second["members"][0]["successful_probe_count"] == 1
|