Files
grok-free-register-oss/tests/test_register_runtime_unittest.py
chaos d10009d639 Initial commit: grok-free-register-oss
Open-source Grok free registration CLI, xai_enroller auth pipeline,
local auth service, tests and docs.
2026-07-16 21:04:05 +08:00

1326 lines
47 KiB
Python

import asyncio
import json
import sys
import tempfile
import types
import unittest
from grok_register.core.observer import Metrics
playwright_pkg = types.ModuleType("playwright")
playwright_async_api = types.ModuleType("playwright.async_api")
playwright_async_api.async_playwright = lambda: None
sys.modules.setdefault("playwright", playwright_pkg)
sys.modules.setdefault("playwright.async_api", playwright_async_api)
requests_mod = types.ModuleType("requests")
requests_mod.get = lambda *_args, **_kwargs: None
requests_mod.post = lambda *_args, **_kwargs: None
sys.modules.setdefault("requests", requests_mod)
from grok_register import register
def test_registration_persists_exact_session_before_legacy_outputs(monkeypatch):
writes = []
monkeypatch.setattr(
register,
"_append_registration_line",
lambda path, line, mode=None, durable=False: writes.append(
(path, line, mode, durable)
),
)
register._persist_registration(
"user@example.test",
"password",
"sso-token",
[{"name": "sso", "value": "opaque", "domain": "accounts.x.ai"}],
)
assert [item[0] for item in writes] == [
"keys/auth-sessions.jsonl",
"keys/accounts.txt",
"keys/grok.txt",
]
assert writes[0][2] == 0o600
assert [item[3] for item in writes] == [True, False, False]
class FakePage:
def __init__(self, context=None):
self.context = context
self.closed = False
self.url = "about:blank"
self.goto_calls = []
self.waits = []
self.evaluations = []
self.route_calls = []
self.mouse = types.SimpleNamespace(
moves=[],
clicks=[],
downs=0,
ups=0,
move=self._mouse_move,
click=self._mouse_click,
down=self._mouse_down,
up=self._mouse_up,
)
self.turnstile_token = ""
self.turnstile_box = {"x": 160, "y": 45}
self.turnstile_page_trace = {
"created_at": 10.0,
"script_inserted_at": 11.0,
"script_loaded_at": 12.0,
"render_called_at": 13.0,
"render_returned_at": 14.0,
"token_written_at": None,
"token_len": 0,
"error": None,
}
self.turnstile_dom_snapshot = {
"widget": {"present": True, "x": 10, "y": 10, "w": 300, "h": 70, "visible": True},
"click_center": {"x": 160, "y": 45},
"element_at_center": {"tag": "IFRAME", "id": "", "class": "", "is_iframe": True},
"all_iframe_count": 1,
"turnstile_iframe_count": 1,
"iframe_summaries": [
{"host": "challenges.cloudflare.com", "path": "/turnstile/v0", "x": 10, "y": 10, "w": 300, "h": 70, "visible": True}
],
"turnstile_loaded": True,
"response_input": {"present": True, "token_len": 0},
}
async def set_viewport_size(self, size):
self.viewport = size
pass
async def goto(self, url, timeout=None, wait_until=None):
self.url = url
self.goto_calls.append({
"url": url,
"timeout": timeout,
"wait_until": wait_until,
})
pass
async def wait_for_timeout(self, timeout):
self.waits.append(timeout)
pass
async def evaluate(self, script):
self.evaluations.append(script)
if "__csp_solver_snapshot" in script:
return self.turnstile_dom_snapshot
if "__cspTurnstileTrace" in script and "return window.__cspTurnstileTrace" in script:
return self.turnstile_page_trace
if "cf-turnstile-response" in script:
return self.turnstile_token
if "getBoundingClientRect" in script and ".cf-turnstile" in script:
return self.turnstile_box
return None
async def close(self):
self.closed = True
pass
async def route(self, pattern, handler):
self.route_calls.append({"pattern": pattern, "handler": handler})
async def _mouse_move(self, x, y, steps=None):
self.mouse.moves.append({"x": x, "y": y, "steps": steps})
async def _mouse_click(self, x, y):
self.mouse.clicks.append({"x": x, "y": y})
async def _mouse_down(self):
self.mouse.downs += 1
async def _mouse_up(self):
self.mouse.ups += 1
class FakeContext:
def __init__(self):
self.pages = []
self.closed = False
self.clear_cookies_calls = 0
self.request = types.SimpleNamespace(get=self._request_get)
self.request_get_calls = []
self.cookies_value = []
self.cancel_on_clear = False
async def new_page(self):
page = FakePage(self)
self.pages.append(page)
return page
async def clear_cookies(self):
self.clear_cookies_calls += 1
if self.cancel_on_clear:
raise asyncio.CancelledError()
self.cookies_value = []
pass
async def cookies(self):
return list(self.cookies_value)
async def _request_get(self, url, timeout=None):
self.request_get_calls.append({"url": url, "timeout": timeout})
return types.SimpleNamespace(status=403)
async def close(self):
self.closed = True
for page in self.pages:
page.closed = True
class FakeBrowser:
def __init__(self):
self.pages = []
self.contexts = []
self.context = types.SimpleNamespace(request=object())
async def new_page(self):
page = FakePage(self.context)
self.pages.append(page)
return page
async def new_context(self):
context = FakeContext()
self.contexts.append(context)
return context
class FakePair:
def __init__(self):
self.t = types.SimpleNamespace(value="tok")
self.q = types.SimpleNamespace(
value={"email": "e@example.test", "password": "pw", "code": "123456"}
)
class FakeInventory:
def __init__(self):
self.active = 0
self.claims = 0
self.t_depth = 0
self.q_depth = 0
def claim_pair(self):
inventory = self
class Claim:
async def __aenter__(self):
inventory.active += 1
inventory.claims += 1
return FakePair()
async def __aexit__(self, exc_type, exc, tb):
inventory.active -= 1
return False
return Claim()
class RegisterRuntimeTests(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
self._old_stop = register.STOP
self._old_timeout = getattr(register, "C_CONSUME_TIMEOUT", None)
self._old_verify = register.grpc_verify_code
self._old_register = register.server_action_register
self._old_create_code = register.grpc_create_code
self._old_poll_code = register.poll_code
self._old_poll_code_async = register._poll_code_async
self._old_log = register.log
self._old_target = register.TARGET
self._old_c_hot_page_pool = getattr(register, "C_HOT_PAGE_POOL", None)
self._old_c_hot_page_pool_size = getattr(register, "C_HOT_PAGE_POOL_SIZE", None)
self._old_c_set_cookie_via_request = getattr(register, "C_SET_COOKIE_VIA_REQUEST", None)
self._old_log_mode = getattr(register, "REGISTER_LOG_MODE", None)
self._old_rate_limit_circuit = register.REGISTRATION_RATE_LIMIT_CIRCUIT
async def asyncTearDown(self):
if hasattr(register, "_close_c_hot_page_pool"):
await register._close_c_hot_page_pool()
register.STOP = self._old_stop
if self._old_timeout is not None:
register.C_CONSUME_TIMEOUT = self._old_timeout
register.grpc_verify_code = self._old_verify
register.server_action_register = self._old_register
register.grpc_create_code = self._old_create_code
register.poll_code = self._old_poll_code
register._poll_code_async = self._old_poll_code_async
register.log = self._old_log
register.TARGET = self._old_target
if self._old_c_hot_page_pool is None and hasattr(register, "C_HOT_PAGE_POOL"):
delattr(register, "C_HOT_PAGE_POOL")
elif self._old_c_hot_page_pool is not None:
register.C_HOT_PAGE_POOL = self._old_c_hot_page_pool
if self._old_c_hot_page_pool_size is None and hasattr(register, "C_HOT_PAGE_POOL_SIZE"):
delattr(register, "C_HOT_PAGE_POOL_SIZE")
elif self._old_c_hot_page_pool_size is not None:
register.C_HOT_PAGE_POOL_SIZE = self._old_c_hot_page_pool_size
if self._old_c_set_cookie_via_request is None and hasattr(register, "C_SET_COOKIE_VIA_REQUEST"):
delattr(register, "C_SET_COOKIE_VIA_REQUEST")
elif self._old_c_set_cookie_via_request is not None:
register.C_SET_COOKIE_VIA_REQUEST = self._old_c_set_cookie_via_request
if self._old_log_mode is None and hasattr(register, "REGISTER_LOG_MODE"):
delattr(register, "REGISTER_LOG_MODE")
elif self._old_log_mode is not None:
register.REGISTER_LOG_MODE = self._old_log_mode
register.REGISTRATION_RATE_LIMIT_CIRCUIT = self._old_rate_limit_circuit
async def test_c_worker_timeout_releases_physical_and_pair_and_counts_failure(self):
async def slow_verify(*_args, **_kwargs):
await asyncio.sleep(10)
return True
register.STOP = asyncio.Event()
register.C_CONSUME_TIMEOUT = 0.05
register.grpc_verify_code = slow_verify
register.server_action_register = lambda *_args, **_kwargs: None
register.log = lambda _msg: None
inventory = FakeInventory()
physical_sem = asyncio.Semaphore(1)
metrics = Metrics()
task = asyncio.create_task(
register.c_worker(0, FakeBrowser(), inventory, physical_sem, metrics)
)
await asyncio.sleep(0.12)
register.STOP.set()
await asyncio.wait_for(task, timeout=1)
self.assertEqual(physical_sem._value, 1)
self.assertEqual(inventory.active, 0)
self.assertEqual(metrics.pair_consumed_fail, 1)
async def test_c_consume_uses_single_use_page_by_default(self):
async def ok_verify(*_args, **_kwargs):
return True
async def no_sso_register(*_args, **_kwargs):
return None
register.C_HOT_PAGE_POOL = False
register.grpc_verify_code = ok_verify
register.server_action_register = no_sso_register
register.log = lambda _msg: None
browser = FakeBrowser()
physical_sem = asyncio.Semaphore(1)
ok = await register._consume_pair(browser, physical_sem, FakePair(), Metrics())
self.assertFalse(ok)
self.assertEqual(len(browser.pages), 1)
self.assertEqual(len(browser.contexts), 0)
self.assertTrue(browser.pages[0].closed)
self.assertEqual(physical_sem._value, 1)
async def test_c_hot_page_reuses_page_and_clears_cookies_between_consumes(self):
seen_pages = []
async def ok_verify(*_args, **_kwargs):
return True
async def no_sso_register(page, *_args, **_kwargs):
seen_pages.append(page)
return None
register.C_HOT_PAGE_POOL = True
register.C_HOT_PAGE_POOL_SIZE = 2
register.grpc_verify_code = ok_verify
register.server_action_register = no_sso_register
register.log = lambda _msg: None
browser = FakeBrowser()
physical_sem = asyncio.Semaphore(1)
first = await register._consume_pair(browser, physical_sem, FakePair(), Metrics())
second = await register._consume_pair(browser, physical_sem, FakePair(), Metrics())
self.assertFalse(first)
self.assertFalse(second)
self.assertEqual(len(browser.contexts), 1)
self.assertEqual(len(browser.contexts[0].pages), 1)
self.assertEqual(seen_pages, [browser.contexts[0].pages[0], browser.contexts[0].pages[0]])
self.assertEqual(browser.contexts[0].clear_cookies_calls, 2)
self.assertFalse(browser.contexts[0].closed)
self.assertFalse(browser.contexts[0].pages[0].closed)
self.assertEqual(physical_sem._value, 1)
async def test_c_hot_page_discards_page_after_exception(self):
async def failing_verify(*_args, **_kwargs):
raise RuntimeError("verify failed")
register.C_HOT_PAGE_POOL = True
register.C_HOT_PAGE_POOL_SIZE = 2
register.grpc_verify_code = failing_verify
register.log = lambda _msg: None
browser = FakeBrowser()
physical_sem = asyncio.Semaphore(1)
with self.assertRaises(RuntimeError):
await register._consume_pair(browser, physical_sem, FakePair(), Metrics())
self.assertEqual(len(browser.contexts), 1)
self.assertTrue(browser.contexts[0].closed)
self.assertTrue(browser.contexts[0].pages[0].closed)
self.assertEqual(physical_sem._value, 1)
async def test_c_hot_page_closes_context_when_cancelled_during_cleanup(self):
context = FakeContext()
page = await context.new_page()
page.url = "https://accounts.x.ai/sign-up?redirect=grok-com"
context.cancel_on_clear = True
register.C_HOT_PAGE_POOL = True
with self.assertRaises(asyncio.CancelledError):
await register._release_c_page(context, page, healthy=True)
self.assertTrue(context.closed)
self.assertTrue(page.closed)
async def test_c_hot_page_discards_page_after_fallback_navigation(self):
context = FakeContext()
page = await context.new_page()
page.url = "https://example.test/set-cookie?q=abc"
register.C_HOT_PAGE_POOL = True
register.C_HOT_PAGE_POOL_SIZE = 2
await register._release_c_page(context, page, healthy=True)
self.assertTrue(context.closed)
self.assertTrue(page.closed)
self.assertEqual(register._c_hot_page_pool, [])
async def test_server_action_can_set_cookie_via_request_without_navigating_page(self):
page = FakePage()
context = FakeContext()
context.cookies_value = [{"name": "sso", "value": "x" * 152}]
page.context = context
page.goto_calls = []
async def fake_evaluate(_script):
return '0:"https:\\/\\/auth.grokipedia.com\\/set-cookie?q=abc"1:'
page.evaluate = fake_evaluate
register.STATE_TREE = "state"
register.ACTION_ID = "action"
register.C_SET_COOKIE_VIA_REQUEST = True
sso = await register.server_action_register(
page, "e@example.test", "pw", "123456", "token"
)
self.assertEqual(sso, "x" * 152)
self.assertEqual(page.goto_calls, [])
self.assertEqual(
context.request_get_calls,
[{"url": "https://auth.grokipedia.com/set-cookie?q=abc", "timeout": 15000}],
)
async def test_server_action_raises_a_distinct_error_for_a_rate_limited_signup_page(self):
page = FakePage()
async def rate_limited_response(_script):
return "Too many requests. Please try again later."
page.evaluate = rate_limited_response
register.STATE_TREE = "state"
register.ACTION_ID = "action"
with self.assertRaises(register.RegistrationRateLimited):
await register.server_action_register(
page, "e@example.test", "pw", "123456", "token"
)
async def test_rate_limit_circuit_opens_for_the_configured_cooldown(self):
now = [100.0]
circuit = register.RegistrationRateLimitCircuit(
cooldown_seconds=60,
clock=lambda: now[0],
)
circuit.trip()
self.assertTrue(circuit.is_open())
self.assertEqual(circuit.remaining_seconds(), 60)
now[0] = 160.0
self.assertFalse(circuit.is_open())
async def test_monitor_uses_metrics_snapshot(self):
register.STOP = asyncio.Event()
register.TARGET = 1
register.REGISTER_LOG_MODE = "debug"
messages = []
register.log = messages.append
metrics = Metrics()
metrics.success_count = 1
metrics.pair_claimed = 2
metrics.pair_consumed_ok = 1
metrics.pair_consumed_fail = 1
sems = {
"physical": asyncio.Semaphore(1),
"t_slot": asyncio.Semaphore(1),
"q_slot": asyncio.Semaphore(1),
"q_pending": asyncio.Semaphore(1),
}
await register.monitor(FakeInventory(), sems, metrics, interval=0)
self.assertTrue(
any("pair:2 ok:1 fail:1" in message for message in messages),
messages,
)
async def test_monitor_hides_internal_snapshot_in_user_mode(self):
register.STOP = asyncio.Event()
register.TARGET = 1
register.REGISTER_LOG_MODE = "user"
messages = []
register.log = messages.append
metrics = Metrics()
metrics.success_count = 1
sems = {
"physical": asyncio.Semaphore(1),
"t_slot": asyncio.Semaphore(1),
"q_slot": asyncio.Semaphore(1),
"q_pending": asyncio.Semaphore(1),
}
await register.monitor(FakeInventory(), sems, metrics, interval=0)
self.assertFalse(any(message.startswith("[*] T:") for message in messages), messages)
async def test_user_event_format_reports_only_registration_outcomes(self):
self.assertEqual(
register.format_user_registration_event("started", task_id=7),
"[→] 开始注册 #7",
)
self.assertEqual(
register.format_user_registration_event(
"success", task_id=7, count=5, rate_per_minute=12.34
),
"[✓] 注册成功 #7 | 运行平均 12.3/分 | 累计 5",
)
self.assertEqual(
register.format_user_registration_event("failed", task_id=7),
"[✗] 注册失败 #7 | 已跳过,继续下一任务",
)
self.assertEqual(
register.format_user_registration_event("rate_limited", wait_seconds=60),
"[⏸] 触发限流 | 60秒后恢复探测",
)
self.assertEqual(
register.format_user_registration_event("recovered", wait_seconds=61),
"[▶] 限流解除 | 实际等待 61秒",
)
async def test_debug_flag_overrides_environment_and_invalid_mode_is_rejected(self):
self.assertEqual(
register.resolve_register_log_mode(["--debug"], {"REGISTER_LOG_MODE": "user"}),
"debug",
)
self.assertEqual(
register.resolve_register_log_mode([], {"REGISTER_LOG_MODE": "debug"}),
"debug",
)
with self.assertRaises(ValueError):
register.resolve_register_log_mode([], {"REGISTER_LOG_MODE": "verbose"})
async def test_registration_task_numbers_are_separate_from_pair_claims(self):
metrics = Metrics()
metrics.pair_claimed = 12
self.assertEqual(metrics.next_registration_task(), 1)
self.assertEqual(metrics.next_registration_task(), 2)
async def test_five_minute_rate_uses_process_uptime_then_sliding_window(self):
now = [0.0]
metrics = Metrics(clock=lambda: now[0])
self.assertIsNone(metrics.five_minute_success_rate())
now[0] = 10.0
metrics.record_success()
now[0] = 20.0
metrics.record_success()
self.assertEqual(metrics.five_minute_success_rate(), 6.0)
now[0] = 311.0
self.assertAlmostEqual(metrics.five_minute_success_rate(), 0.2)
now[0] = 321.0
self.assertEqual(metrics.five_minute_success_rate(), 0.0)
async def test_runtime_average_rate_includes_the_entire_cooldown_period(self):
now = [0.0]
metrics = Metrics(clock=lambda: now[0])
self.assertIsNone(metrics.runtime_average_success_rate())
now[0] = 10.0
metrics.record_success()
now[0] = 20.0
metrics.record_success()
self.assertEqual(metrics.runtime_average_success_rate(), 6.0)
now[0] = 320.0
self.assertEqual(metrics.runtime_average_success_rate(), 0.375)
async def test_terminal_output_failure_does_not_escape_log(self):
old_output = register._terminal_output
try:
register._terminal_output = lambda _message: (_ for _ in ()).throw(
OSError("closed pipe")
)
register.log("safe")
finally:
register._terminal_output = old_output
async def test_rate_limit_circuit_measures_one_recovery_window(self):
now = [100.0]
circuit = register.RegistrationRateLimitCircuit(
cooldown_seconds=60,
clock=lambda: now[0],
)
circuit.trip()
now[0] = 161.5
self.assertEqual(circuit.consume_recovery_seconds(), 61.5)
self.assertIsNone(circuit.consume_recovery_seconds())
async def test_rate_limit_probe_is_released_when_consume_times_out(self):
async def slow_verify(*_args, **_kwargs):
await asyncio.Event().wait()
register.REGISTRATION_RATE_LIMIT_CIRCUIT = register.RegistrationRateLimitCircuit(0)
register.REGISTRATION_RATE_LIMIT_CIRCUIT.trip()
register.grpc_verify_code = slow_verify
with self.assertRaises(asyncio.TimeoutError):
await asyncio.wait_for(
register._consume_pair(
FakeBrowser(), asyncio.Semaphore(1), FakePair(), Metrics(), task_id=1
),
timeout=0.01,
)
self.assertFalse(register.REGISTRATION_RATE_LIMIT_CIRCUIT._probe_active)
async def test_auto_capacity_is_bounded_by_cpu_and_memory(self):
roomy = register.derive_capacity(
cpu_count=2,
max_mem_mb=5600,
physical_cap=0,
physical_per_cpu=4,
physical_mem_mb=512,
min_free_mem_mb=500,
)
tight = register.derive_capacity(
cpu_count=2,
max_mem_mb=1100,
physical_cap=0,
physical_per_cpu=4,
physical_mem_mb=512,
min_free_mem_mb=500,
)
self.assertEqual(roomy[0], 8)
self.assertEqual(roomy[1], 10)
self.assertEqual(roomy[2], register.Q_PENDING_CAP + 2)
self.assertEqual(roomy[3], 10)
self.assertEqual(tight[0], 1)
async def test_default_auto_capacity_is_conservative(self):
physical, s_workers, _p_workers, c_workers = register.derive_capacity(
cpu_count=2,
max_mem_mb=5600,
physical_cap=0,
)
self.assertEqual(physical, 4)
self.assertEqual(s_workers, 6)
self.assertEqual(c_workers, 6)
async def test_explicit_physical_cap_overrides_auto_capacity(self):
physical, s_workers, _p_workers, c_workers = register.derive_capacity(
cpu_count=2,
max_mem_mb=1100,
physical_cap=3,
physical_per_cpu=4,
physical_mem_mb=512,
min_free_mem_mb=500,
)
self.assertEqual(physical, 3)
self.assertEqual(s_workers, 5)
self.assertEqual(c_workers, 5)
async def test_capacity_profile_supplies_physical_cap_when_not_explicit(self):
physical, s_workers, _p_workers, c_workers = register.derive_capacity(
cpu_count=2,
max_mem_mb=5600,
physical_cap=0,
profile_physical_cap=7,
)
tight, *_ = register.derive_capacity(
cpu_count=2,
max_mem_mb=1100,
physical_cap=0,
profile_physical_cap=7,
)
explicit, *_ = register.derive_capacity(
cpu_count=2,
max_mem_mb=5600,
physical_cap=5,
profile_physical_cap=7,
)
self.assertEqual(physical, 7)
self.assertEqual(s_workers, 9)
self.assertEqual(c_workers, 9)
self.assertEqual(tight, 1)
self.assertEqual(explicit, 5)
async def test_load_capacity_profile_reads_valid_physical_cap(self):
with tempfile.NamedTemporaryFile("w+", delete=True) as f:
json.dump({"physical_cap": 7}, f)
f.flush()
profile = register.load_capacity_profile(f.name)
self.assertEqual(profile["physical_cap"], 7)
self.assertEqual(register.load_capacity_profile("/does/not/exist"), {})
self.assertEqual(register.load_capacity_profile(""), {})
async def test_admission_t_high_defaults_to_physical_cap_bounded_by_slot(self):
watermarks = register.derive_admission_watermarks(
physical_cap=6,
t_slot_cap=8,
q_pending_cap=12,
t_target=4,
q_target=4,
)
bounded = register.derive_admission_watermarks(
physical_cap=10,
t_slot_cap=8,
q_pending_cap=12,
t_target=4,
q_target=4,
)
self.assertEqual(watermarks["t_high"], 6)
self.assertEqual(watermarks["t_low"], 3)
self.assertEqual(bounded["t_high"], 8)
async def test_admission_t_high_override_remains_explicit(self):
watermarks = register.derive_admission_watermarks(
physical_cap=6,
t_slot_cap=8,
q_pending_cap=12,
t_target=4,
q_target=4,
t_high_override=4,
t_low_override=2,
)
self.assertEqual(watermarks["t_high"], 4)
self.assertEqual(watermarks["t_low"], 2)
async def test_c_hot_page_pool_size_is_derived_at_startup(self):
self.assertEqual(
register.derive_c_hot_page_pool_size(
physical_cap=6, c_workers=8, configured_size=0
),
6,
)
self.assertEqual(
register.derive_c_hot_page_pool_size(
physical_cap=8, c_workers=3, configured_size=0
),
3,
)
self.assertEqual(
register.derive_c_hot_page_pool_size(
physical_cap=8, c_workers=10, configured_size=4
),
4,
)
async def test_send_q_request_batch_reuses_one_page_for_multiple_emails(self):
emails = []
async def fake_create_code(_page, email):
emails.append(email)
return True
register.grpc_create_code = fake_create_code
browser = FakeBrowser()
physical_sem = asyncio.Semaphore(1)
p_send_sem = asyncio.Semaphore(1)
requests = [
{"handle": "h1", "email": "a@example.test", "password": "pw1"},
{"handle": "h2", "email": "b@example.test", "password": "pw2"},
{"handle": "h3", "email": "c@example.test", "password": "pw3"},
]
results = await register._send_q_request_batch(
browser, physical_sem, p_send_sem, requests
)
self.assertEqual(emails, [item["email"] for item in requests])
self.assertEqual([item["sent"] for item in results], [True, True, True])
self.assertEqual(len(browser.pages), 1)
self.assertTrue(browser.pages[0].closed)
self.assertEqual(physical_sem._value, 1)
self.assertEqual(p_send_sem._value, 1)
async def test_send_q_request_batch_records_physical_and_stage_metrics(self):
async def fake_create_code(_page, _email):
return True
register.grpc_create_code = fake_create_code
browser = FakeBrowser()
physical_sem = asyncio.Semaphore(1)
p_send_sem = asyncio.Semaphore(1)
metrics = Metrics()
await register._send_q_request_batch(
browser,
physical_sem,
p_send_sem,
[{"handle": "h1", "email": "a@example.test", "password": "pw"}],
metrics,
)
self.assertEqual(metrics.p_physical_count, 1)
self.assertEqual(metrics.p_page_prepare_count, 1)
self.assertEqual(metrics.p_send_count, 1)
self.assertGreaterEqual(metrics.p_physical_wait_seconds, 0)
self.assertGreaterEqual(metrics.p_physical_hold_seconds, 0)
async def test_consume_pair_records_physical_and_stage_metrics(self):
async def ok_verify(*_args, **_kwargs):
return True
async def no_sso_register(*_args, **_kwargs):
return None
register.C_HOT_PAGE_POOL = False
register.grpc_verify_code = ok_verify
register.server_action_register = no_sso_register
register.log = lambda _msg: None
metrics = Metrics()
await register._consume_pair(
FakeBrowser(), asyncio.Semaphore(1), FakePair(), metrics
)
self.assertEqual(metrics.c_physical_count, 1)
self.assertEqual(metrics.c_page_acquire_count, 1)
self.assertEqual(metrics.c_verify_count, 1)
self.assertEqual(metrics.c_register_count, 1)
self.assertEqual(metrics.c_hot_page_hits, 0)
self.assertEqual(metrics.c_hot_page_misses, 0)
async def test_poll_and_admit_q_releases_one_pending_per_terminal_request(self):
register.P_REQUEST_TIMEOUT = 1
register.poll_code = lambda _handle: None
q_pending_sem = asyncio.Semaphore(0)
q_slot_sem = asyncio.Semaphore(1)
metrics = Metrics()
await register._poll_and_admit_q(
{"handle": "h1", "email": "a@example.test", "password": "pw"},
FakeInventory(),
q_pending_sem,
q_slot_sem,
metrics,
)
self.assertEqual(q_pending_sem._value, 1)
self.assertEqual(q_slot_sem._value, 1)
self.assertEqual(metrics.q_discarded, 1)
async def test_poll_cancel_before_terminal_does_not_release_pending(self):
async def blocked_poll(_loop, _handle):
await asyncio.sleep(10)
return "123456"
register._poll_code_async = blocked_poll
q_pending_sem = asyncio.Semaphore(0)
q_slot_sem = asyncio.Semaphore(1)
metrics = Metrics()
task = asyncio.create_task(
register._poll_and_admit_q(
{"handle": "h1", "email": "a@example.test", "password": "pw"},
FakeInventory(),
q_pending_sem,
q_slot_sem,
metrics,
)
)
await asyncio.sleep(0.02)
task.cancel()
with self.assertRaises(asyncio.CancelledError):
await task
self.assertEqual(q_pending_sem._value, 0)
self.assertEqual(q_slot_sem._value, 1)
async def test_poll_cancel_after_q_return_releases_pending(self):
async def returned_poll(_loop, _handle):
return "123456"
class BlockingInventory(FakeInventory):
async def put_q(self, _env):
await asyncio.sleep(10)
register._poll_code_async = returned_poll
q_pending_sem = asyncio.Semaphore(0)
q_slot_sem = asyncio.Semaphore(1)
metrics = Metrics()
task = asyncio.create_task(
register._poll_and_admit_q(
{"handle": "h1", "email": "a@example.test", "password": "pw"},
BlockingInventory(),
q_pending_sem,
q_slot_sem,
metrics,
)
)
await asyncio.sleep(0.02)
task.cancel()
with self.assertRaises(asyncio.CancelledError):
await task
self.assertEqual(q_pending_sem._value, 1)
self.assertEqual(q_slot_sem._value, 1)
self.assertEqual(metrics.q_returned, 1)
async def test_metrics_snapshot_includes_solver_timing(self):
metrics = Metrics()
metrics.t_solve_count = 2
metrics.t_solve_seconds = 5.0
metrics.t_solve_failed = 1
sems = {
"physical": asyncio.Semaphore(1),
"t_slot": asyncio.Semaphore(1),
"q_slot": asyncio.Semaphore(1),
"q_pending": asyncio.Semaphore(1),
}
row = metrics.snapshot(FakeInventory(), sems)
self.assertIn("t_solve_avg:2.5", row)
self.assertIn("t_solve_fail:1", row)
async def test_metrics_snapshot_includes_role_physical_and_stage_timing(self):
metrics = Metrics()
metrics.s_physical_count = 2
metrics.s_physical_wait_seconds = 1.0
metrics.s_physical_hold_seconds = 6.0
metrics.p_physical_count = 1
metrics.p_physical_wait_seconds = 0.2
metrics.p_physical_hold_seconds = 1.4
metrics.c_physical_count = 3
metrics.c_physical_wait_seconds = 0.9
metrics.c_physical_hold_seconds = 7.5
metrics.p_email_create_count = 2
metrics.p_email_create_seconds = 1.0
metrics.p_page_prepare_count = 1
metrics.p_page_prepare_seconds = 0.8
metrics.p_send_count = 1
metrics.p_send_seconds = 0.4
metrics.c_page_acquire_count = 2
metrics.c_page_acquire_seconds = 0.6
metrics.c_verify_count = 2
metrics.c_verify_seconds = 0.8
metrics.c_register_count = 2
metrics.c_register_seconds = 3.0
metrics.c_hot_page_hits = 4
metrics.c_hot_page_misses = 1
sems = {
"physical": asyncio.Semaphore(1),
"t_slot": asyncio.Semaphore(1),
"q_slot": asyncio.Semaphore(1),
"q_pending": asyncio.Semaphore(1),
}
row = metrics.snapshot(FakeInventory(), sems)
self.assertIn("s_phys:0.50/3.00", row)
self.assertIn("p_phys:0.20/1.40", row)
self.assertIn("c_phys:0.30/2.50", row)
self.assertIn("p_stage:0.50/0.80/0.40", row)
self.assertIn("c_stage:0.30/0.40/1.50", row)
self.assertIn("c_hot:4/1", row)
async def test_record_solver_trace_accumulates_stage_metrics(self):
metrics = Metrics()
register._record_solver_trace(
metrics,
{
"goto_s": 1.0,
"inject_s": 0.2,
"initial_s": 0.5,
"click_s": 0.1,
"wait_s": 20.0,
"reused": True,
"visible_frame": False,
},
21.8,
"token",
)
register._record_solver_trace(metrics, {}, 10.0, None)
self.assertEqual(metrics.t_solve_count, 2)
self.assertEqual(metrics.t_solve_failed, 1)
self.assertAlmostEqual(metrics.solver_goto_seconds, 1.0)
self.assertAlmostEqual(metrics.solver_wait_seconds, 20.0)
self.assertEqual(metrics.solver_reused_count, 1)
self.assertEqual(metrics.solver_visible_frame_count, 0)
async def test_solver_timeline_records_click_and_token_events_when_enabled(self):
page = FakePage()
original_click = register._mouse_click_turnstile_center_trace
original_read = register._read_turnstile_token
clicked_once = False
timeline = register._new_solver_timeline(enabled=True)
async def fake_click(_page, **_kwargs):
nonlocal clicked_once
clicked_once = True
return True, {"box_eval_ms": 0.0}
async def fake_read(_page):
return "token-value-long" if clicked_once else ""
try:
register._mouse_click_turnstile_center_trace = fake_click
register._read_turnstile_token = fake_read
clicked = await register._repeat_mouse_click_turnstile(page, timeline=timeline)
finally:
register._mouse_click_turnstile_center_trace = original_click
register._read_turnstile_token = original_read
events = [event["event"] for event in timeline["events"]]
self.assertTrue(clicked)
self.assertIn("click_before", events)
self.assertIn("click_after", events)
self.assertTrue(any(event.get("dom", {}).get("widget", {}).get("present") for event in timeline["events"]))
self.assertTrue(any(event.get("click_call_ms", 0) >= 0 for event in timeline["events"]))
self.assertTrue(any(event.get("token_len", 0) > 10 for event in timeline["events"]))
async def test_turnstile_dom_snapshot_reports_click_target_without_text_or_full_urls(self):
page = FakePage()
snapshot = await register._turnstile_dom_snapshot(page)
self.assertEqual(snapshot["all_iframe_count"], 1)
self.assertEqual(snapshot["turnstile_iframe_count"], 1)
self.assertEqual(snapshot["widget"]["w"], 300)
self.assertEqual(snapshot["element_at_center"]["tag"], "IFRAME")
self.assertEqual(snapshot["iframe_summaries"][0]["host"], "challenges.cloudflare.com")
self.assertNotIn("src", snapshot["iframe_summaries"][0])
self.assertNotIn("text", snapshot["element_at_center"])
async def test_mouse_click_turnstile_center_can_return_timing_trace(self):
page = FakePage()
clicked, trace = await register._mouse_click_turnstile_center_trace(page)
self.assertTrue(clicked)
self.assertEqual(trace["click_x"], 160.0)
self.assertEqual(trace["click_y"], 45.0)
for key in ("box_eval_ms", "mouse_move1_ms", "mouse_move2_ms", "mouse_down_ms", "mouse_up_ms"):
self.assertIn(key, trace)
self.assertGreaterEqual(trace[key], 0)
async def test_inject_turnstile_widget_leaves_default_script_uninstrumented(self):
page = FakePage()
await register._inject_turnstile_widget(page)
script = page.evaluations[-1]
self.assertNotIn("__cspTurnstileTrace", script)
async def test_inject_turnstile_widget_records_page_timeline_when_enabled(self):
page = FakePage()
await register._inject_turnstile_widget(page, timeline=True)
script = page.evaluations[-1]
self.assertIn("__cspTurnstileTrace", script)
self.assertIn("script_inserted_at", script)
self.assertIn("render_called_at", script)
self.assertIn("token_written_at", script)
async def test_start_turnstile_challenge_records_page_trace_when_timeline_enabled(self):
browser = FakeBrowser()
messages = []
old_trace = register.SOLVER_TIMELINE_TRACE
old_sample = register.SOLVER_TIMELINE_SAMPLE
old_emitted = register._solver_timeline_emitted
old_log = register.log
try:
register.SOLVER_TIMELINE_TRACE = True
register.SOLVER_TIMELINE_SAMPLE = 1
register._solver_timeline_emitted = 0
register.log = messages.append
item = await register._start_turnstile_challenge(browser, fast_click=True)
await register._put_solver_page(item, False)
finally:
register.SOLVER_TIMELINE_TRACE = old_trace
register.SOLVER_TIMELINE_SAMPLE = old_sample
register._solver_timeline_emitted = old_emitted
register.log = old_log
events = item["timeline"]["events"]
self.assertTrue(any(event["event"] == "page_trace_after_inject" for event in events))
self.assertTrue(any(event["event"] == "page_trace_after_click" for event in events))
async def test_solve_one_turnstile_uses_fast_click_by_default(self):
calls = []
async def fake_start(_browser, *, fast_click=False):
calls.append(fast_click)
return {"page": object()}
async def fake_wait(_item):
return "token-value"
old_start = register._start_turnstile_challenge
old_wait = register._wait_turnstile_challenge
try:
register._start_turnstile_challenge = fake_start
register._wait_turnstile_challenge = fake_wait
token = await register.solve_one_turnstile(object())
finally:
register._start_turnstile_challenge = old_start
register._wait_turnstile_challenge = old_wait
self.assertEqual(token, "token-value")
self.assertEqual(calls, [True])
async def test_wait_turnstile_logs_timeline_when_present(self):
messages = []
register.REGISTER_LOG_MODE = "debug"
async def fake_poll(_page, **_kwargs):
return "token-value-long"
async def fake_put(_item, _ok):
return None
old_poll = register._poll_turnstile_token
old_put = register._put_solver_page
old_log = register.log
try:
register._poll_turnstile_token = fake_poll
register._put_solver_page = fake_put
register.log = messages.append
item = {
"page": object(),
"trace": {},
"timeline": {
"start": register.time.time(),
"events": [{"t": 0.1, "event": "x"}],
},
}
token = await register._wait_turnstile_challenge(item)
finally:
register._poll_turnstile_token = old_poll
register._put_solver_page = old_put
register.log = old_log
self.assertEqual(token, "token-value-long")
self.assertTrue(any(message.startswith("[solver_timeline] ") for message in messages))
async def test_wait_turnstile_timeline_logs_solve_id_and_poll_summary(self):
messages = []
register.REGISTER_LOG_MODE = "debug"
page = FakePage()
page.turnstile_token = "token-value-long"
timeline = register._new_solver_timeline(enabled=True)
async def fake_put(_item, _ok):
return None
old_attempts = register.SOLVER_POLL_ATTEMPTS
old_interval = register.SOLVER_POLL_INTERVAL_MS
old_put = register._put_solver_page
old_sleep = register.asyncio.sleep
old_log = register.log
try:
register.SOLVER_POLL_ATTEMPTS = 1
register.SOLVER_POLL_INTERVAL_MS = 50
async def no_sleep(_seconds):
return None
register.asyncio.sleep = no_sleep
register._put_solver_page = fake_put
register.log = messages.append
token = await register._wait_turnstile_challenge({
"page": page,
"trace": {},
"timeline": timeline,
})
finally:
register.SOLVER_POLL_ATTEMPTS = old_attempts
register.SOLVER_POLL_INTERVAL_MS = old_interval
register.asyncio.sleep = old_sleep
register._put_solver_page = old_put
register.log = old_log
payload = next(message.removeprefix("[solver_timeline] ") for message in messages)
events = register.json.loads(payload)
poll_done = next(event for event in events if event["event"] == "poll_done")
self.assertEqual(token, "token-value-long")
self.assertIn("solve_id", poll_done)
self.assertEqual(poll_done["poll_attempts"], 1)
self.assertEqual(poll_done["first_token_attempt"], 1)
self.assertGreaterEqual(poll_done["poll_read_ms_max"], 0)
async def test_mouse_click_turnstile_retries_uses_center_clicks(self):
page = FakePage()
old_retries = getattr(register, "SOLVER_MOUSE_CLICK_RETRIES", None)
old_interval = getattr(register, "SOLVER_MOUSE_CLICK_INTERVAL_MS", None)
old_sleep = register.asyncio.sleep
try:
register.SOLVER_MOUSE_CLICK_RETRIES = 3
register.SOLVER_MOUSE_CLICK_INTERVAL_MS = 600
async def no_sleep(_seconds):
return None
register.asyncio.sleep = no_sleep
clicked = await register._repeat_mouse_click_turnstile(page)
finally:
register.asyncio.sleep = old_sleep
if old_retries is None:
delattr(register, "SOLVER_MOUSE_CLICK_RETRIES")
else:
register.SOLVER_MOUSE_CLICK_RETRIES = old_retries
if old_interval is None:
delattr(register, "SOLVER_MOUSE_CLICK_INTERVAL_MS")
else:
register.SOLVER_MOUSE_CLICK_INTERVAL_MS = old_interval
self.assertTrue(clicked)
self.assertEqual(page.mouse.downs, 3)
self.assertEqual(page.mouse.ups, 3)
self.assertEqual(page.mouse.moves[-1], {"x": 160, "y": 45, "steps": 8})
async def test_mouse_click_turnstile_stops_when_token_appears(self):
page = FakePage()
evaluate_count = 0
original_evaluate = page.evaluate
async def evaluate(script):
nonlocal evaluate_count
if "cf-turnstile-response" in script:
evaluate_count += 1
return "token-value-long" if evaluate_count > 1 else ""
return await original_evaluate(script)
page.evaluate = evaluate
old_retries = getattr(register, "SOLVER_MOUSE_CLICK_RETRIES", None)
old_interval = getattr(register, "SOLVER_MOUSE_CLICK_INTERVAL_MS", None)
old_sleep = register.asyncio.sleep
try:
register.SOLVER_MOUSE_CLICK_RETRIES = 3
register.SOLVER_MOUSE_CLICK_INTERVAL_MS = 600
async def no_sleep(_seconds):
return None
register.asyncio.sleep = no_sleep
clicked = await register._repeat_mouse_click_turnstile(page)
finally:
register.asyncio.sleep = old_sleep
if old_retries is None:
delattr(register, "SOLVER_MOUSE_CLICK_RETRIES")
else:
register.SOLVER_MOUSE_CLICK_RETRIES = old_retries
if old_interval is None:
delattr(register, "SOLVER_MOUSE_CLICK_INTERVAL_MS")
else:
register.SOLVER_MOUSE_CLICK_INTERVAL_MS = old_interval
self.assertTrue(clicked)
self.assertEqual(page.mouse.downs, 1)
self.assertEqual(evaluate_count, 2)
async def test_prepare_signup_page_uses_configured_navigation_profile(self):
page = FakePage()
old_wait_until = getattr(register, "PAGE_GOTO_WAIT_UNTIL", None)
old_post_wait = getattr(register, "PAGE_POST_WAIT_MS", None)
try:
register.PAGE_GOTO_WAIT_UNTIL = "domcontentloaded"
register.PAGE_POST_WAIT_MS = 500
await register._prepare_signup_page(page, redirect=True)
finally:
if old_wait_until is None:
delattr(register, "PAGE_GOTO_WAIT_UNTIL")
else:
register.PAGE_GOTO_WAIT_UNTIL = old_wait_until
if old_post_wait is None:
delattr(register, "PAGE_POST_WAIT_MS")
else:
register.PAGE_POST_WAIT_MS = old_post_wait
self.assertEqual(page.goto_calls[-1]["wait_until"], "domcontentloaded")
self.assertEqual(page.waits[-1], 500)
async def test_prepare_signup_page_can_block_static_assets(self):
page = FakePage()
old_block_assets = getattr(register, "PAGE_BLOCK_STATIC_ASSETS", None)
try:
register.PAGE_BLOCK_STATIC_ASSETS = True
await register._prepare_signup_page(page, redirect=True)
finally:
if old_block_assets is None:
delattr(register, "PAGE_BLOCK_STATIC_ASSETS")
else:
register.PAGE_BLOCK_STATIC_ASSETS = old_block_assets
self.assertEqual(len(page.route_calls), 1)
self.assertEqual(page.route_calls[0]["pattern"], "**/*")
async def test_default_solver_and_page_latency_profile_matches_accepted_optimization(self):
self.assertEqual(register.SOLVER_INITIAL_WAIT_MS, 500)
self.assertTrue(register.SOLVER_FAST_CLICK)
self.assertEqual(register.SOLVER_MOUSE_CLICK_RETRIES, 3)
self.assertEqual(register.SOLVER_MOUSE_CLICK_INTERVAL_MS, 600)
self.assertEqual(register.PAGE_GOTO_WAIT_UNTIL, "domcontentloaded")
self.assertEqual(register.PAGE_POST_WAIT_MS, 500)
if __name__ == "__main__":
unittest.main()