From c6d349c5053ae603208f463958e7bcfed61d2c4b Mon Sep 17 00:00:00 2001 From: Wong Ding Feng Date: Mon, 1 Jun 2026 00:02:38 +0800 Subject: [PATCH] feat: Config and pluggable auth stub (manual/none) --- src/auto_reverse/config.py | 61 ++++++++++++++++++++++++++++++++++++++ tests/test_config.py | 27 +++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/auto_reverse/config.py create mode 100644 tests/test_config.py diff --git a/src/auto_reverse/config.py b/src/auto_reverse/config.py new file mode 100644 index 0000000..8b47b5f --- /dev/null +++ b/src/auto_reverse/config.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Protocol +from urllib.parse import urlsplit + + +@dataclass +class Config: + target_url: str + out_dir: str | None = None + proxy_port: int = 8080 + headless: bool = False + profile: str | None = None + gen_client: bool = False + model: str = "claude-opus-4-8" + scope_hosts: set[str] = field(default_factory=set[str]) + no_llm_doc: bool = False + resume: str | None = None + auth: str = "manual" + + @property + def target_host(self) -> str: + return urlsplit(self.target_url).hostname or "" + + def all_scope_hosts(self) -> set[str]: + return {self.target_host, *self.scope_hosts} + + +class AuthStrategy(Protocol): + name: str + + async def authenticate(self, page: object) -> None: + """Prepare an authenticated session on the given Playwright page.""" + ... + + +class NoAuth: + name = "none" + + async def authenticate(self, page: object) -> None: + return None + + +class ManualPauseAuth: + """Default stub: pause so the human can log in by hand, then continue.""" + + name = "manual" + + async def authenticate(self, page: object) -> None: + # Implemented against the real page in browser.py wiring; the stub + # simply records intent. The REPL prompts the user to log in and + # press enter before autonomous exploration begins. + return None + + +def make_auth(name: str) -> AuthStrategy: + strategies: dict[str, AuthStrategy] = {"manual": ManualPauseAuth(), "none": NoAuth()} + if name not in strategies: + raise ValueError(f"unknown auth strategy: {name!r}") + return strategies[name] diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..57b61ef --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,27 @@ +import pytest + +from auto_reverse.config import Config, ManualPauseAuth, NoAuth, make_auth + + +def test_config_derives_target_host(): + cfg = Config(target_url="https://app.example.com/dashboard") + assert cfg.target_host == "app.example.com" + + +def test_config_scope_hosts_includes_target_plus_extra(): + cfg = Config(target_url="https://app.example.com", scope_hosts={"api.example.com"}) + assert cfg.all_scope_hosts() == {"app.example.com", "api.example.com"} + + +def test_default_model(): + assert Config(target_url="https://x.com").model == "claude-opus-4-8" + + +def test_make_auth_returns_strategy(): + assert isinstance(make_auth("manual"), ManualPauseAuth) + assert isinstance(make_auth("none"), NoAuth) + + +def test_make_auth_unknown_raises(): + with pytest.raises(ValueError): + make_auth("oauth-magic")