Initial commit: grok-free-register-oss

Open-source Grok free registration CLI, xai_enroller auth pipeline,
local auth service, tests and docs.
This commit is contained in:
chaos
2026-07-16 21:04:05 +08:00
commit d10009d639
72 changed files with 18752 additions and 0 deletions

475
tests/test_cancel.py Normal file
View File

@@ -0,0 +1,475 @@
"""
Layer 2: 取消注入测试
在每个 await 边界前后强制取消 Worker,验证:
- S 取消后 Physical_Sem 和 T_Slot_Sem 不泄漏
- P 取消后 Physical_Sem、Q_Pending_Sem、Q_Slot_Sem 不泄漏
- C 取消后 Physical_Sem 不泄漏,已 claim 的 T/Q 被核销
- 库存守恒: 取消不导致 slot 无界堆积或耗尽
"""
import asyncio
import time
import pytest
from grok_register.core.envelope import ResourceEnvelope
from grok_register.core.inventory import Inventory
from grok_register.core.observer import Metrics
from tests.fakes import FakeTurnstile, FakeEmailService, FakeRegisterAPI, Conservation
# ═══════════════════════════════════════════
# S_Worker 取消测试
# ═══════════════════════════════════════════
async def _s_worker_impl(browser, inventory, physical_sem, t_slot_sem, metrics, stop):
"""S_Worker 简化实现,用于测试。"""
while not stop.is_set():
await physical_sem.acquire()
try:
token = await browser.generate()
finally:
physical_sem.release()
if token is None:
metrics.t_discarded += 1
await asyncio.sleep(0.05)
continue
metrics.t_produced += 1
now = time.time()
try:
t_env = await ResourceEnvelope.create_with_slot(
'T', token, t_slot_sem, expires_at=now + 300
)
except asyncio.CancelledError:
metrics.t_discarded += 1
raise
try:
await inventory.put_t(t_env)
except Exception:
t_env.discard(reason='put_failed')
metrics.t_discarded += 1
await asyncio.sleep(0.02)
class TestSCancel:
"""S_Worker 取消语义测试。"""
@pytest.mark.asyncio
async def test_cancel_during_generate_no_leak(self):
"""S 在 generate() 期间被取消,sem 不泄漏。"""
ft = FakeTurnstile(delay=0.5) # 慢生成
inv = Inventory()
phys = asyncio.Semaphore(2)
t_slot = asyncio.Semaphore(4)
metrics = Metrics()
stop = asyncio.Event()
task = asyncio.create_task(
_s_worker_impl(ft, inv, phys, t_slot, metrics, stop)
)
await asyncio.sleep(0.05) # 等它进入 generate
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# Physical_Sem 应完全释放
assert phys._value == 2, f'Physical_Sem 泄漏: {phys._value}'
# T_Slot_Sem 不变(没生成成功)
assert t_slot._value == 4
@pytest.mark.asyncio
async def test_cancel_during_put_t_no_leak(self):
"""S 在 put_t 期间被取消(极端: put 内部取消),envelope 被 discard。"""
inv = Inventory()
phys = asyncio.Semaphore(2)
t_slot = asyncio.Semaphore(4)
metrics = Metrics()
stop = asyncio.Event()
# 快速生成
ft = FakeTurnstile(delay=0.001)
task = asyncio.create_task(
_s_worker_impl(ft, inv, phys, t_slot, metrics, stop)
)
# 在 put_t 之前的极短窗口取消
await asyncio.sleep(0.002)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
assert phys._value == 2
# t_slot 可能是 4 或 3,但不应是负数
assert t_slot._value >= 0
assert t_slot._value <= 4
@pytest.mark.asyncio
async def test_s_repeated_cancel_no_accumulation(self):
"""反复取消 S 多次,sem 计数不累积。"""
ft = FakeTurnstile(delay=0.01)
inv = Inventory()
phys = asyncio.Semaphore(2)
t_slot = asyncio.Semaphore(4)
metrics = Metrics()
stop = asyncio.Event()
for _ in range(20):
task = asyncio.create_task(
_s_worker_impl(ft, inv, phys, t_slot, metrics, stop)
)
await asyncio.sleep(0.02)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
assert phys._value == 2, f'Physical_Sem 泄漏: {phys._value}'
# 守恒: free + inventory == total (成功入库的 envelope 占着 slot)
assert t_slot._value + inv.t_depth == 4, f'T_Slot 守恒违反: free={t_slot._value} inv={inv.t_depth}'
# ═══════════════════════════════════════════
# P_Worker 取消测试
# ═══════════════════════════════════════════
async def _p_worker_impl(email_svc, inventory, physical_sem, q_pending_sem,
q_slot_sem, metrics, stop):
"""P_Worker 简化实现,用于测试。"""
loop = asyncio.get_event_loop()
while not stop.is_set():
await q_pending_sem.acquire()
_pending_released = False
try:
await physical_sem.acquire()
try:
handle, email, password = await email_svc.create_email()
sent = True # fake 总是成功
finally:
physical_sem.release()
if not sent:
q_pending_sem.release()
_pending_released = True
continue
metrics.q_sent += 1
try:
code = await asyncio.wait_for(
email_svc.poll_code(handle), timeout=95
)
except asyncio.TimeoutError:
code = None
if code is None:
metrics.q_discarded += 1
q_pending_sem.release()
_pending_released = True
continue
metrics.q_returned += 1
now = time.time()
q_env = await ResourceEnvelope.create_with_slot(
'Q', {'email': email, 'password': password, 'code': code},
q_slot_sem, expires_at=now + 120,
)
try:
await inventory.put_q(q_env)
except Exception:
q_env.discard(reason='put_failed')
metrics.q_discarded += 1
except asyncio.CancelledError:
if not _pending_released:
q_pending_sem.release()
_pending_released = True
raise
except Exception:
metrics.q_discarded += 1
finally:
if not _pending_released:
q_pending_sem.release()
await asyncio.sleep(0.02)
class TestPCancel:
"""P_Worker 取消语义测试。"""
@pytest.mark.asyncio
async def test_cancel_during_create_email(self):
"""P 在 create_email 期间被取消,所有 sem 正确释放。"""
es = FakeEmailService()
es.set_cancel_create_at(1)
inv = Inventory()
phys = asyncio.Semaphore(2)
q_pend = asyncio.Semaphore(4)
q_slot = asyncio.Semaphore(4)
metrics = Metrics()
stop = asyncio.Event()
task = asyncio.create_task(
_p_worker_impl(es, inv, phys, q_pend, q_slot, metrics, stop)
)
await asyncio.sleep(0.1)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
assert phys._value == 2, f'Physical_Sem 泄漏: {phys._value}'
assert q_pend._value == 4, f'Q_Pending 泄漏: {q_pend._value}'
assert q_slot._value == 4
@pytest.mark.asyncio
async def test_cancel_during_poll_code(self):
"""P 在 poll_code 期间被取消,不持有 Physical_Sem。"""
es = FakeEmailService()
es.set_cancel_poll_at(1)
inv = Inventory()
phys = asyncio.Semaphore(2)
q_pend = asyncio.Semaphore(4)
q_slot = asyncio.Semaphore(4)
metrics = Metrics()
stop = asyncio.Event()
task = asyncio.create_task(
_p_worker_impl(es, inv, phys, q_pend, q_slot, metrics, stop)
)
await asyncio.sleep(0.1)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# P 等 Q 时不持有 Physical_Sem
assert phys._value == 2, f'Physical_Sem 应完全释放: {phys._value}'
assert q_pend._value == 4, f'Q_Pending 泄漏: {q_pend._value}'
@pytest.mark.asyncio
async def test_p_repeated_cancel_no_accumulation(self):
"""反复取消 P 多次,sem 计数不累积。"""
es = FakeEmailService()
inv = Inventory()
phys = asyncio.Semaphore(2)
q_pend = asyncio.Semaphore(4)
q_slot = asyncio.Semaphore(4)
metrics = Metrics()
stop = asyncio.Event()
for _ in range(20):
task = asyncio.create_task(
_p_worker_impl(es, inv, phys, q_pend, q_slot, metrics, stop)
)
await asyncio.sleep(0.03)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
assert phys._value == 2, f'Physical_Sem 泄漏: {phys._value}'
assert q_pend._value == 4, f'Q_Pending 泄漏: {q_pend._value}'
# 守恒: free + inventory == total
assert q_slot._value + inv.q_depth == 4, f'Q_Slot 守恒违反: free={q_slot._value} inv={inv.q_depth}'
# ═══════════════════════════════════════════
# C_Worker 取消测试
# ═══════════════════════════════════════════
async def _c_worker_impl(register_api, browser_unused, inventory, physical_sem,
metrics, stop, file_lock):
"""C_Worker 简化实现,用于测试。"""
while not stop.is_set():
try:
async with inventory.claim_pair() as pair:
t_val = pair.t.value
q_val = pair.q.value
await physical_sem.acquire()
try:
sso = await register_api.register(
q_val['email'], q_val['password'], q_val['code'], t_val
)
if sso:
metrics.pair_consumed_ok += 1
metrics.success_count += 1
else:
metrics.pair_consumed_fail += 1
finally:
physical_sem.release()
except asyncio.CancelledError:
raise
except Exception:
metrics.pair_consumed_fail += 1
await asyncio.sleep(0.02)
class TestCCancel:
"""C_Worker 取消语义测试。"""
@pytest.mark.asyncio
async def test_cancel_during_register_pair_settled(self):
"""C 在 register 期间被取消,pair 已 claim,必须核销。"""
fra = FakeRegisterAPI(delay=0.5)
inv = Inventory()
phys = asyncio.Semaphore(2)
t_slot = asyncio.Semaphore(4)
q_slot = asyncio.Semaphore(4)
metrics = Metrics()
stop = asyncio.Event()
fl = asyncio.Lock()
# 用带 metrics 的 inventory
inv_with_metrics = Inventory(metrics=Metrics())
t_env = await ResourceEnvelope.create_with_slot('T', 'tok', t_slot)
q_env = await ResourceEnvelope.create_with_slot('Q', {'email': 'a@b.com', 'password': 'p', 'code': '123'}, q_slot)
await inv_with_metrics.put_t(t_env)
await inv_with_metrics.put_q(q_env)
task = asyncio.create_task(
_c_worker_impl(fra, None, inv_with_metrics, phys, metrics, stop, fl)
)
await asyncio.sleep(0.1) # 等它 claim 进入 register
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# pair 已核销: slot 应释放
assert t_slot._value == 4, f'T slot 未核销: {t_slot._value}'
assert q_slot._value == 4, f'Q slot 未核销: {q_slot._value}'
assert phys._value == 2, f'Physical_Sem 泄漏: {phys._value}'
@pytest.mark.asyncio
async def test_cancel_during_claim_wait_no_pop(self):
"""C 在 claim_pair 等待时取消,不弹出资源。"""
inv = Inventory()
phys = asyncio.Semaphore(2)
t_slot = asyncio.Semaphore(4)
q_slot = asyncio.Semaphore(4)
metrics = Metrics()
stop = asyncio.Event()
fl = asyncio.Lock()
fra = FakeRegisterAPI()
# 放一个 T 但不放 Q,让 C 等待
t_env = await ResourceEnvelope.create_with_slot('T', 'tok', t_slot)
await inv.put_t(t_env)
task = asyncio.create_task(
_c_worker_impl(fra, None, inv, phys, metrics, stop, fl)
)
await asyncio.sleep(0.1)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# T 仍在 inventory 中
assert inv.t_depth == 1, 'claim 等待时取消不应弹出 T'
assert phys._value == 2
@pytest.mark.asyncio
async def test_c_repeated_cancel_no_accumulation(self):
"""反复取消 C 多次,sem 计数不累积。"""
fra = FakeRegisterAPI(delay=0.01)
inv = Inventory()
phys = asyncio.Semaphore(2)
t_slot = asyncio.Semaphore(10)
q_slot = asyncio.Semaphore(10)
metrics = Metrics()
stop = asyncio.Event()
fl = asyncio.Lock()
for _ in range(10):
# 每次放一对
t_env = await ResourceEnvelope.create_with_slot('T', f'tok_{_}', t_slot)
q_env = await ResourceEnvelope.create_with_slot('Q', {'email': f'e@{_}', 'password': 'p', 'code': '123'}, q_slot)
await inv.put_t(t_env)
await inv.put_q(q_env)
task = asyncio.create_task(
_c_worker_impl(fra, None, inv, phys, metrics, stop, fl)
)
await asyncio.sleep(0.03)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
assert phys._value == 2, f'Physical_Sem 泄漏: {phys._value}'
# 所有 slot 应已核销
assert t_slot._value == 10, f'T_Slot 泄漏: {t_slot._value}'
assert q_slot._value == 10, f'Q_Slot 泄漏: {q_slot._value}'
# ═══════════════════════════════════════════
# 端到端取消: S+P+C 同时运行时取消
# ═══════════════════════════════════════════
class TestE2ECancel:
"""端到端取消测试: S/P/C 同时运行时取消,验证全局守恒。"""
@pytest.mark.asyncio
async def test_cancel_all_workers_sem_reset(self):
"""同时启动 S/P/C,然后全部取消,所有 sem 恢复初始值。"""
ft = FakeTurnstile(delay=0.05)
es = FakeEmailService()
fra = FakeRegisterAPI(delay=0.05)
inv = Inventory()
phys = asyncio.Semaphore(4)
t_slot = asyncio.Semaphore(8)
q_slot = asyncio.Semaphore(8)
q_pend = asyncio.Semaphore(12)
metrics = Metrics()
stop = asyncio.Event()
fl = asyncio.Lock()
tasks = []
for _ in range(3):
tasks.append(asyncio.create_task(
_s_worker_impl(ft, inv, phys, t_slot, metrics, stop)
))
tasks.append(asyncio.create_task(
_p_worker_impl(es, inv, phys, q_pend, q_slot, metrics, stop)
))
tasks.append(asyncio.create_task(
_c_worker_impl(fra, None, inv, phys, metrics, stop, fl)
))
await asyncio.sleep(0.2)
stop.set()
for t in tasks:
t.cancel()
for t in tasks:
try:
await t
except asyncio.CancelledError:
pass
# 全部停止后,守恒检查: free + inventory == total
assert phys._value >= 0 and phys._value <= 4
assert t_slot._value + inv.t_depth == 8, f'T 守恒违反: free={t_slot._value} inv={inv.t_depth}'
assert q_slot._value + inv.q_depth == 8, f'Q 守恒违反: free={q_slot._value} inv={inv.q_depth}'
assert q_pend._value >= 0 and q_pend._value <= 12
# 守恒检查
cons = Conservation(t_slot, q_slot, q_pend, inv)
cons.check_t_conservation(8)
cons.check_q_conservation(8)
cons.check_pending_conservation(12)