Files
last30days-skill/tests/test_setup_openclaw.py
Matt Van Horn 0a9ff16dfc feat: v3.0.0 - intelligent search, GitHub person/project mode, ELI5, 13+ sources
v3 rewrites the search engine from the ground up:

- Intelligent pre-research: resolves X handles, GitHub repos, subreddits,
  TikTok hashtags, and YouTube channels before searching
- GitHub person-mode: PR velocity, top repos by stars, release notes
- GitHub project-mode: live star counts, README, releases, top issues
- ELI5 mode: plain language synthesis, no jargon
- 13+ sources: Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket,
  GitHub, Threads, Pinterest, Perplexity, Bluesky, Web
- Free Reddit comments via public JSON (no API key needed)
- Fun judge v2: humor scoring baked into narrative
- Cookie consent before browser scanning
- 10,000 free ScrapeCreators calls
- 1,012 tests

Thank you to the community contributors whose issues and PRs shaped v3:
@uppinote20 (#143), @zerone0x (#134, #136), @thinkun (#116),
@thomasmktong (#124), @fanispoulinakisai-boop (#100), @pejmanjohn (#78),
@zl190 (#115), @hnshah (#84, #85, #86)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:52:23 -07:00

563 lines
21 KiB
Python

"""Tests for OpenClaw setup and device auth functions."""
import json
import sys
import time
from pathlib import Path
from unittest.mock import patch, MagicMock, call
import pytest
# Add scripts dir to path
SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
sys.path.insert(0, str(SCRIPTS_DIR))
from lib import setup_wizard
class TestRunOpenclawSetup:
"""Tests for run_openclaw_setup()."""
@patch("shutil.which")
def test_all_tools_present_no_keys(self, mock_which):
"""All CLI tools found, no API keys configured."""
mock_which.side_effect = lambda cmd: f"/usr/bin/{cmd}"
config = {}
result = setup_wizard.run_openclaw_setup(config)
assert result["yt_dlp"] is True
assert result["node"] is True
assert result["python3"] is True
assert all(v is False for v in result["keys"].values())
assert result["x_method"] is None
@patch("shutil.which")
def test_missing_tools(self, mock_which):
"""Some CLI tools missing."""
def which_side(cmd):
if cmd == "node":
return None
return f"/usr/bin/{cmd}"
mock_which.side_effect = which_side
config = {}
result = setup_wizard.run_openclaw_setup(config)
assert result["yt_dlp"] is True
assert result["node"] is False
assert result["python3"] is True
@patch("shutil.which")
def test_keys_detected(self, mock_which):
"""API keys in config are reported as present."""
mock_which.return_value = None
config = {
"XAI_API_KEY": "xai-abc123",
"BRAVE_API_KEY": "brav-xyz",
"SCRAPECREATORS_API_KEY": "", # empty = falsy
}
result = setup_wizard.run_openclaw_setup(config)
assert result["keys"]["xai"] is True
assert result["keys"]["brave"] is True
assert result["keys"]["scrapecreators"] is False
@patch("shutil.which")
def test_x_method_xai(self, mock_which):
"""x_method is 'xai' when XAI_API_KEY is set."""
mock_which.return_value = None
config = {"XAI_API_KEY": "xai-key"}
result = setup_wizard.run_openclaw_setup(config)
assert result["x_method"] == "xai"
@patch("shutil.which")
def test_x_method_cookies(self, mock_which):
"""x_method is 'cookies' when AUTH_TOKEN + CT0 are set."""
mock_which.return_value = None
config = {"AUTH_TOKEN": "tok", "CT0": "ct0val"}
result = setup_wizard.run_openclaw_setup(config)
assert result["x_method"] == "cookies"
@patch("shutil.which")
def test_x_method_xai_over_cookies(self, mock_which):
"""XAI takes priority over cookies for x_method."""
mock_which.return_value = None
config = {"XAI_API_KEY": "xai-key", "AUTH_TOKEN": "tok", "CT0": "ct0val"}
result = setup_wizard.run_openclaw_setup(config)
assert result["x_method"] == "xai"
@patch("shutil.which")
def test_x_method_null_when_nothing(self, mock_which):
"""x_method is None when no X access configured."""
mock_which.return_value = None
config = {}
result = setup_wizard.run_openclaw_setup(config)
assert result["x_method"] is None
@patch("shutil.which")
def test_output_is_json_serializable(self, mock_which):
"""Result can be serialized to JSON without errors."""
mock_which.return_value = "/usr/bin/something"
config = {"XAI_API_KEY": "k", "OPENAI_API_KEY": "ok"}
result = setup_wizard.run_openclaw_setup(config)
serialized = json.dumps(result)
parsed = json.loads(serialized)
assert parsed["yt_dlp"] is True
assert parsed["keys"]["xai"] is True
class TestRunDeviceAuth:
"""Tests for run_device_auth()."""
@patch("lib.setup_wizard.urlopen")
def test_success(self, mock_urlopen):
"""Successful device code request returns tuple."""
resp_data = {
"device_code": "dc-123",
"user_code": "ABCD-1234",
"verification_uri": "https://github.com/login/device",
"interval": 5,
}
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(resp_data).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
mock_urlopen.return_value = mock_resp
result = setup_wizard.run_device_auth()
assert result is not None
device_code, user_code, verification_uri, interval = result
assert device_code == "dc-123"
assert user_code == "ABCD-1234"
assert verification_uri == "https://github.com/login/device"
assert interval == 5
@patch("lib.setup_wizard.urlopen")
def test_http_error_returns_none(self, mock_urlopen):
"""HTTP error during code request returns None."""
from urllib.error import HTTPError
mock_urlopen.side_effect = HTTPError(
"https://example.com", 500, "Server Error", {}, None
)
result = setup_wizard.run_device_auth()
assert result is None
@patch("lib.setup_wizard.urlopen")
def test_missing_device_code_returns_none(self, mock_urlopen):
"""Incomplete response (no device_code) returns None."""
resp_data = {"user_code": "ABCD-1234"}
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(resp_data).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
mock_urlopen.return_value = mock_resp
result = setup_wizard.run_device_auth()
assert result is None
class TestPollDeviceAuth:
"""Tests for poll_device_auth()."""
@patch("lib.setup_wizard.time")
@patch("lib.setup_wizard.urlopen")
def test_success_on_second_poll(self, mock_urlopen, mock_time):
"""Returns access_token after initial pending then success."""
# First call: time check (within deadline), second: after sleep, etc.
mock_time.time = MagicMock(side_effect=[0, 0, 0, 0])
mock_time.sleep = MagicMock()
pending_resp = MagicMock()
pending_resp.read.return_value = json.dumps({"error": "authorization_pending"}).encode()
pending_resp.__enter__ = lambda s: s
pending_resp.__exit__ = MagicMock(return_value=False)
success_resp = MagicMock()
success_resp.read.return_value = json.dumps({"access_token": "gho_abc123"}).encode()
success_resp.__enter__ = lambda s: s
success_resp.__exit__ = MagicMock(return_value=False)
mock_urlopen.side_effect = [pending_resp, success_resp]
result = setup_wizard.poll_device_auth("dc-123", interval=1, timeout=300)
assert result == "gho_abc123"
@patch("lib.setup_wizard.time")
@patch("lib.setup_wizard.urlopen")
def test_timeout_returns_none(self, mock_urlopen, mock_time):
"""Returns None when timeout is exceeded."""
# Simulate time passing beyond deadline
mock_time.time = MagicMock(side_effect=[0, 301])
mock_time.sleep = MagicMock()
result = setup_wizard.poll_device_auth("dc-123", interval=5, timeout=300)
assert result is None
@patch("lib.setup_wizard.time")
@patch("lib.setup_wizard.urlopen")
def test_expired_token_returns_none(self, mock_urlopen, mock_time):
"""Returns None on expired_token error."""
mock_time.time = MagicMock(side_effect=[0, 0])
mock_time.sleep = MagicMock()
expired_resp = MagicMock()
expired_resp.read.return_value = json.dumps({"error": "expired_token"}).encode()
expired_resp.__enter__ = lambda s: s
expired_resp.__exit__ = MagicMock(return_value=False)
mock_urlopen.return_value = expired_resp
result = setup_wizard.poll_device_auth("dc-123", interval=1, timeout=300)
assert result is None
@patch("lib.setup_wizard.time")
@patch("lib.setup_wizard.urlopen")
def test_http_400_continues_polling(self, mock_urlopen, mock_time):
"""HTTP 400 during polling continues (authorization pending)."""
from urllib.error import HTTPError
mock_time.time = MagicMock(side_effect=[0, 0, 0])
mock_time.sleep = MagicMock()
success_resp = MagicMock()
success_resp.read.return_value = json.dumps({"access_token": "gho_ok"}).encode()
success_resp.__enter__ = lambda s: s
success_resp.__exit__ = MagicMock(return_value=False)
mock_urlopen.side_effect = [
HTTPError("url", 400, "Bad Request", {}, None),
success_resp,
]
result = setup_wizard.poll_device_auth("dc-123", interval=1, timeout=300)
assert result == "gho_ok"
class TestFetchApiKey:
"""Tests for fetch_api_key()."""
@patch("lib.setup_wizard.urlopen")
def test_success(self, mock_urlopen):
"""Returns api_key from profile response."""
resp_data = {"api_key": "sc-key-abc123", "username": "testuser"}
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(resp_data).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
mock_urlopen.return_value = mock_resp
result = setup_wizard.fetch_api_key("gho_token")
assert result == "sc-key-abc123"
@patch("lib.setup_wizard.urlopen")
def test_no_api_key_in_response(self, mock_urlopen):
"""Returns None when api_key is not in the response."""
resp_data = {"username": "testuser"}
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(resp_data).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
mock_urlopen.return_value = mock_resp
result = setup_wizard.fetch_api_key("gho_token")
assert result is None
@patch("lib.setup_wizard.urlopen")
def test_http_error_returns_none(self, mock_urlopen):
"""HTTP error returns None."""
from urllib.error import HTTPError
mock_urlopen.side_effect = HTTPError(
"https://example.com", 401, "Unauthorized", {}, None
)
result = setup_wizard.fetch_api_key("bad_token")
assert result is None
class TestRunFullDeviceAuth:
"""Tests for run_full_device_auth()."""
@patch("lib.setup_wizard.fetch_api_key")
@patch("lib.setup_wizard.poll_device_auth")
@patch("lib.setup_wizard.run_device_auth")
@patch("webbrowser.open")
def test_happy_path(self, mock_browser, mock_start, mock_poll, mock_fetch):
"""Full flow succeeds: start -> poll -> fetch -> return api_key."""
mock_start.return_value = ("dev123", "ABCD-1234", "https://example.com/device", 5)
mock_poll.return_value = "access_tok"
mock_fetch.return_value = "sc_live_abc123"
result = setup_wizard.run_full_device_auth(timeout=10)
assert result["status"] == "success"
assert result["api_key"] == "sc_live_abc123"
assert result["user_code"] == "ABCD-1234"
mock_browser.assert_called_once_with("https://example.com/device")
@patch("lib.setup_wizard.run_device_auth")
def test_start_fails(self, mock_start):
"""Device code request fails -> error status."""
mock_start.return_value = None
result = setup_wizard.run_full_device_auth()
assert result["status"] == "error"
assert "Failed to start" in result["message"]
@patch("lib.setup_wizard.poll_device_auth")
@patch("lib.setup_wizard.run_device_auth")
@patch("webbrowser.open")
def test_poll_timeout(self, mock_browser, mock_start, mock_poll):
"""Poll times out -> timeout status with user_code."""
mock_start.return_value = ("dev123", "WXYZ-5678", "https://example.com/device", 5)
mock_poll.return_value = None
result = setup_wizard.run_full_device_auth(timeout=10)
assert result["status"] == "timeout"
assert result["user_code"] == "WXYZ-5678"
@patch("lib.setup_wizard.fetch_api_key")
@patch("lib.setup_wizard.poll_device_auth")
@patch("lib.setup_wizard.run_device_auth")
@patch("webbrowser.open")
def test_fetch_fails_after_auth(self, mock_browser, mock_start, mock_poll, mock_fetch):
"""Auth succeeds but profile fetch fails -> error status."""
mock_start.return_value = ("dev123", "CODE-1111", "https://example.com/device", 5)
mock_poll.return_value = "access_tok"
mock_fetch.return_value = None
result = setup_wizard.run_full_device_auth(timeout=10)
assert result["status"] == "error"
assert "failed to fetch" in result["message"].lower()
@patch("lib.setup_wizard.run_device_auth")
@patch("webbrowser.open")
def test_browser_open_fails_gracefully(self, mock_browser, mock_start):
"""webbrowser.open raises -> flow continues without crashing."""
mock_start.return_value = ("dev123", "CODE-2222", "https://example.com/device", 5)
mock_browser.side_effect = Exception("no display")
with patch("lib.setup_wizard.poll_device_auth", return_value=None):
result = setup_wizard.run_full_device_auth(timeout=1)
# Should not crash, just timeout
assert result["status"] == "timeout"
@patch("lib.setup_wizard.run_device_auth")
@patch("webbrowser.open")
def test_no_verification_uri_skips_browser(self, mock_browser, mock_start):
"""Empty verification_uri -> browser not opened."""
mock_start.return_value = ("dev123", "CODE-3333", "", 5)
with patch("lib.setup_wizard.poll_device_auth", return_value=None):
setup_wizard.run_full_device_auth(timeout=1)
mock_browser.assert_not_called()
class TestAuthWithPat:
"""Tests for auth_with_pat()."""
@patch("lib.setup_wizard.urlopen")
def test_success(self, mock_urlopen):
"""Valid PAT -> returns dict with api_key."""
resp = MagicMock()
resp.read.return_value = json.dumps({
"api_key": "sc_live_test123",
"github_username": "testuser",
"credits_remaining": 100,
}).encode()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
mock_urlopen.return_value = resp
result = setup_wizard.auth_with_pat("gho_validtoken")
assert result is not None
assert result["api_key"] == "sc_live_test123"
assert result["github_username"] == "testuser"
@patch("lib.setup_wizard.urlopen")
def test_invalid_token_returns_none(self, mock_urlopen):
"""HTTP 401 (invalid token) -> returns None."""
from urllib.error import HTTPError
mock_urlopen.side_effect = HTTPError(
url="https://api.scrapecreators.com/v1/github/pat/auth",
code=401, msg="Unauthorized", hdrs={}, fp=None,
)
result = setup_wizard.auth_with_pat("gho_badtoken")
assert result is None
@patch("lib.setup_wizard.urlopen")
def test_insufficient_scope_returns_none(self, mock_urlopen):
"""HTTP 422 (insufficient scope) -> returns None."""
from urllib.error import HTTPError
mock_urlopen.side_effect = HTTPError(
url="https://api.scrapecreators.com/v1/github/pat/auth",
code=422, msg="Unprocessable", hdrs={}, fp=None,
)
result = setup_wizard.auth_with_pat("gho_noscope")
assert result is None
@patch("lib.setup_wizard.urlopen")
def test_network_error_returns_none(self, mock_urlopen):
"""URLError -> returns None."""
from urllib.error import URLError
mock_urlopen.side_effect = URLError("Connection refused")
result = setup_wizard.auth_with_pat("gho_anytoken")
assert result is None
@patch("lib.setup_wizard.urlopen")
def test_no_api_key_in_response(self, mock_urlopen):
"""Response without api_key -> returns None."""
resp = MagicMock()
resp.read.return_value = json.dumps({"error": "something"}).encode()
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
mock_urlopen.return_value = resp
result = setup_wizard.auth_with_pat("gho_validtoken")
assert result is None
class TestClipboardDeviceAuth:
"""Tests for clipboard-first behavior in run_full_device_auth()."""
@patch("lib.setup_wizard.run_device_auth")
@patch("lib.setup_wizard.poll_device_auth", return_value=None)
@patch("webbrowser.open")
@patch("subprocess.run")
def test_pbcopy_called_on_macos(self, mock_subproc, mock_browser, mock_poll, mock_start):
"""On macOS, pbcopy is called with the user code before browser opens."""
mock_start.return_value = ("dev123", "CLIP-CODE", "https://github.com/login/device", 5)
with patch("sys.platform", "darwin"):
setup_wizard.run_full_device_auth(timeout=1)
mock_subproc.assert_called_once()
call_args = mock_subproc.call_args
assert call_args[0][0] == ["pbcopy"]
assert call_args[1]["input"] == b"CLIP-CODE"
@patch("lib.setup_wizard.run_device_auth")
@patch("lib.setup_wizard.poll_device_auth", return_value=None)
@patch("webbrowser.open")
@patch("subprocess.run")
def test_no_pbcopy_on_linux(self, mock_subproc, mock_browser, mock_poll, mock_start):
"""On Linux, subprocess.run (pbcopy) is not called."""
mock_start.return_value = ("dev123", "CLIP-CODE", "https://github.com/login/device", 5)
with patch("sys.platform", "linux"):
setup_wizard.run_full_device_auth(timeout=1)
mock_subproc.assert_not_called()
@patch("lib.setup_wizard.run_device_auth")
@patch("lib.setup_wizard.poll_device_auth", return_value=None)
@patch("webbrowser.open")
@patch("subprocess.run", side_effect=Exception("pbcopy not found"))
def test_pbcopy_failure_continues(self, mock_subproc, mock_browser, mock_poll, mock_start):
"""pbcopy failing -> flow continues, browser still opens."""
mock_start.return_value = ("dev123", "CLIP-CODE", "https://github.com/login/device", 5)
with patch("sys.platform", "darwin"):
result = setup_wizard.run_full_device_auth(timeout=1)
# Should not crash, browser still called
mock_browser.assert_called_once()
assert result["status"] == "timeout"
class TestRunGithubAuth:
"""Tests for run_github_auth() — PAT-first with device fallback."""
@patch("lib.setup_wizard.auth_with_pat")
@patch("subprocess.run")
@patch("shutil.which", return_value="/usr/local/bin/gh")
def test_pat_success(self, mock_which, mock_subproc, mock_pat):
"""gh found + valid token + PAT endpoint success -> pat method."""
mock_subproc.return_value = MagicMock(
returncode=0, stdout="gho_testtoken123\n",
)
mock_pat.return_value = {
"api_key": "sc_live_fromPAT",
"github_username": "testuser",
}
result = setup_wizard.run_github_auth(timeout=10)
assert result["status"] == "success"
assert result["method"] == "pat"
assert result["api_key"] == "sc_live_fromPAT"
@patch("lib.setup_wizard.run_full_device_auth")
@patch("lib.setup_wizard.auth_with_pat", return_value=None)
@patch("subprocess.run")
@patch("shutil.which", return_value="/usr/local/bin/gh")
def test_pat_fails_falls_to_device(self, mock_which, mock_subproc, mock_pat, mock_device):
"""gh found + PAT endpoint fails -> falls through to device flow."""
mock_subproc.return_value = MagicMock(
returncode=0, stdout="gho_badtoken\n",
)
mock_device.return_value = {
"status": "success", "method": "device",
"api_key": "sc_live_fromDevice",
}
result = setup_wizard.run_github_auth(timeout=10)
assert result["status"] == "success"
assert result["method"] == "device"
mock_device.assert_called_once()
@patch("lib.setup_wizard.run_full_device_auth")
@patch("shutil.which", return_value=None)
def test_no_gh_goes_to_device(self, mock_which, mock_device):
"""gh not installed -> straight to device flow."""
mock_device.return_value = {
"status": "success", "method": "device",
"api_key": "sc_live_deviceOnly",
}
result = setup_wizard.run_github_auth(timeout=10)
assert result["status"] == "success"
assert result["method"] == "device"
@patch("lib.setup_wizard.run_full_device_auth")
@patch("subprocess.run")
@patch("shutil.which", return_value="/usr/local/bin/gh")
def test_gh_not_logged_in_falls_to_device(self, mock_which, mock_subproc, mock_device):
"""gh exists but not logged in (exit code 1) -> device flow."""
mock_subproc.return_value = MagicMock(
returncode=1, stdout="", stderr="not logged in",
)
mock_device.return_value = {
"status": "success", "method": "device",
"api_key": "sc_live_fallback",
}
result = setup_wizard.run_github_auth(timeout=10)
assert result["status"] == "success"
assert result["method"] == "device"