4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
282 lines
10 KiB
Python
282 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
|
|
import pytest
|
|
from rich.console import Console
|
|
|
|
from integrations.github import login as github_login_mod
|
|
from integrations.github.login import GitHubLoginResult
|
|
from integrations.github.mcp import DEFAULT_GITHUB_MCP_TOOLSETS, DEFAULT_GITHUB_MCP_URL
|
|
from integrations.github.mcp_oauth import GitHubDeviceCode
|
|
from platform.analytics import source as analytics_source
|
|
from platform.terminal import theme as ui_theme
|
|
from surfaces.interactive_shell.runtime.startup import first_launch_github as flg
|
|
|
|
|
|
def _console() -> Console:
|
|
return Console(file=io.StringIO(), force_terminal=False, highlight=False)
|
|
|
|
|
|
def _terminal_console(output: io.StringIO) -> Console:
|
|
return Console(file=output, force_terminal=True, color_system="truecolor", highlight=False)
|
|
|
|
|
|
def _force_required(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Set every gate input so that login would be required."""
|
|
monkeypatch.delenv("OPENSRE_SKIP_GITHUB_LOGIN", raising=False)
|
|
monkeypatch.setattr(flg, "is_test_run", lambda: False)
|
|
monkeypatch.setattr(flg, "repl_tty_interactive", lambda: True)
|
|
monkeypatch.setattr(flg, "_github_already_configured", lambda: False)
|
|
monkeypatch.setattr(flg, "read_github_login_deferred", lambda: False)
|
|
|
|
|
|
def test_gate_required_when_all_conditions_met(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_force_required(monkeypatch)
|
|
assert flg.should_require_github_login() is True
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["1", "true", "YES", "on"])
|
|
def test_gate_skipped_by_env(monkeypatch: pytest.MonkeyPatch, value: str) -> None:
|
|
_force_required(monkeypatch)
|
|
monkeypatch.setenv("OPENSRE_SKIP_GITHUB_LOGIN", value)
|
|
assert flg.should_require_github_login() is False
|
|
|
|
|
|
def test_gate_skipped_in_test_run(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_force_required(monkeypatch)
|
|
monkeypatch.setattr(flg, "is_test_run", lambda: True)
|
|
assert flg.should_require_github_login() is False
|
|
|
|
|
|
def test_gate_required_on_linux_when_interactive(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_force_required(monkeypatch)
|
|
assert flg.should_require_github_login() is True
|
|
|
|
|
|
def test_gate_skipped_when_not_tty(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_force_required(monkeypatch)
|
|
monkeypatch.setattr(flg, "repl_tty_interactive", lambda: False)
|
|
assert flg.should_require_github_login() is False
|
|
|
|
|
|
def test_gate_skipped_when_github_already_configured(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_force_required(monkeypatch)
|
|
monkeypatch.setattr(flg, "_github_already_configured", lambda: True)
|
|
assert flg.should_require_github_login() is False
|
|
|
|
|
|
def test_gate_required_when_github_not_configured(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Regression: GitHub config is authoritative. A prior completed login (no
|
|
longer recorded via any standalone marker) must not let the REPL start once
|
|
the GitHub integration has been removed."""
|
|
_force_required(monkeypatch)
|
|
monkeypatch.setattr(flg, "_github_already_configured", lambda: False)
|
|
assert flg.should_require_github_login() is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ci_env",
|
|
[
|
|
("CI", "true"),
|
|
("GITHUB_ACTIONS", "true"),
|
|
("OPENSRE_IS_TEST", "1"),
|
|
],
|
|
)
|
|
def test_gate_skipped_in_ci_like_environment(
|
|
monkeypatch: pytest.MonkeyPatch, ci_env: tuple[str, str]
|
|
) -> None:
|
|
monkeypatch.delenv("OPENSRE_SKIP_GITHUB_LOGIN", raising=False)
|
|
monkeypatch.delenv("CI", raising=False)
|
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
|
monkeypatch.delenv("OPENSRE_IS_TEST", raising=False)
|
|
monkeypatch.delenv("OPENSRE_INVESTIGATION_SOURCE", raising=False)
|
|
monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
|
|
monkeypatch.setattr(flg, "is_test_run", analytics_source.is_test_run)
|
|
monkeypatch.setattr(flg, "repl_tty_interactive", lambda: True)
|
|
monkeypatch.setattr(flg, "_github_already_configured", lambda: False)
|
|
monkeypatch.setenv(ci_env[0], ci_env[1])
|
|
assert flg.should_require_github_login() is False
|
|
|
|
|
|
def test_gate_skipped_when_github_login_deferred(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_force_required(monkeypatch)
|
|
monkeypatch.setattr(flg, "read_github_login_deferred", lambda: True)
|
|
assert flg.should_require_github_login() is False
|
|
|
|
|
|
def test_gate_required_when_stale_github_store_record_has_no_token(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Legacy installs may have an abandoned github store row without credentials."""
|
|
_force_required(monkeypatch)
|
|
monkeypatch.setattr(
|
|
"integrations.store.get_integration",
|
|
lambda service: (
|
|
{
|
|
"credentials": {
|
|
"mode": "streamable-http",
|
|
"url": DEFAULT_GITHUB_MCP_URL,
|
|
"toolsets": list(DEFAULT_GITHUB_MCP_TOOLSETS),
|
|
}
|
|
}
|
|
if service == "github"
|
|
else None
|
|
),
|
|
)
|
|
assert flg.should_require_github_login() is True
|
|
|
|
|
|
def test_device_code_prompt_highlights_user_code(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
monkeypatch.delenv("FORCE_COLOR", raising=False)
|
|
ui_theme.set_active_theme("blue")
|
|
output = io.StringIO()
|
|
code = GitHubDeviceCode(
|
|
device_code="dev-123",
|
|
user_code="WXYZ-1234",
|
|
verification_uri="https://github.com/login/device",
|
|
expires_in=900,
|
|
interval=5,
|
|
)
|
|
|
|
flg._show_device_code(_terminal_console(output), code)
|
|
|
|
rendered = output.getvalue()
|
|
assert f"{ui_theme.DEVICE_CODE_ANSI}WXYZ-1234" in rendered
|
|
|
|
|
|
def test_orchestrator_success_proceeds_and_propagates(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: True)
|
|
monkeypatch.setattr(
|
|
github_login_mod,
|
|
"authenticate_and_configure_github",
|
|
lambda **_kwargs: GitHubLoginResult(ok=True, username="octocat", detail="OK"),
|
|
)
|
|
completed: list[str] = []
|
|
monkeypatch.setattr(flg, "capture_github_login_completed", completed.append)
|
|
cleared: list[bool] = []
|
|
monkeypatch.setattr(flg, "clear_github_login_deferral", lambda: cleared.append(True))
|
|
|
|
proceed = flg.require_github_login_on_first_launch(_console())
|
|
|
|
assert proceed is True
|
|
assert completed == ["octocat"]
|
|
assert cleared == [True]
|
|
|
|
|
|
def test_orchestrator_skip_at_menu_proceeds(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: False)
|
|
deferred: list[bool] = []
|
|
monkeypatch.setattr(flg, "write_github_login_deferred", deferred.append)
|
|
|
|
proceed = flg.require_github_login_on_first_launch(_console())
|
|
|
|
assert proceed is True
|
|
assert deferred == [True]
|
|
|
|
|
|
def test_orchestrator_escape_during_wait_proceeds(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: True)
|
|
|
|
def _raise_cancel(**_kwargs: object) -> GitHubLoginResult:
|
|
raise KeyboardInterrupt
|
|
|
|
monkeypatch.setattr(github_login_mod, "authenticate_and_configure_github", _raise_cancel)
|
|
deferred: list[bool] = []
|
|
monkeypatch.setattr(flg, "write_github_login_deferred", deferred.append)
|
|
|
|
proceed = flg.require_github_login_on_first_launch(_console())
|
|
|
|
assert proceed is True
|
|
assert deferred == [True]
|
|
|
|
|
|
def test_orchestrator_failure_then_decline_retry_skips(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: True)
|
|
monkeypatch.setattr(
|
|
github_login_mod,
|
|
"authenticate_and_configure_github",
|
|
lambda **_kwargs: GitHubLoginResult(ok=False, detail="cannot verify"),
|
|
)
|
|
monkeypatch.setattr(flg, "_ask_retry", lambda _console: False)
|
|
deferred: list[bool] = []
|
|
monkeypatch.setattr(flg, "write_github_login_deferred", deferred.append)
|
|
|
|
proceed = flg.require_github_login_on_first_launch(_console())
|
|
|
|
assert proceed is True
|
|
assert deferred == [True]
|
|
|
|
|
|
def test_orchestrator_retries_until_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(flg, "_offer_github_login", lambda _console: True)
|
|
calls = {"n": 0}
|
|
|
|
def _login(**_kwargs: object) -> GitHubLoginResult:
|
|
calls["n"] += 1
|
|
if calls["n"] == 1:
|
|
return GitHubLoginResult(ok=False, detail="cannot verify")
|
|
return GitHubLoginResult(ok=True, username="octocat", detail="OK")
|
|
|
|
monkeypatch.setattr(github_login_mod, "authenticate_and_configure_github", _login)
|
|
monkeypatch.setattr(flg, "_ask_retry", lambda _console: True)
|
|
monkeypatch.setattr(flg, "capture_github_login_completed", lambda _username: None)
|
|
monkeypatch.setattr(flg, "clear_github_login_deferral", lambda: None)
|
|
|
|
proceed = flg.require_github_login_on_first_launch(_console())
|
|
|
|
assert proceed is True
|
|
assert calls["n"] == 2
|
|
|
|
|
|
def test_clear_github_login_deferral_noop_when_not_deferred(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(flg, "read_github_login_deferred", lambda: False)
|
|
writes: list[bool] = []
|
|
monkeypatch.setattr(flg, "write_github_login_deferred", writes.append)
|
|
|
|
flg.clear_github_login_deferral()
|
|
|
|
assert writes == []
|
|
|
|
|
|
def test_clear_github_login_deferral_clears_when_deferred(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(flg, "read_github_login_deferred", lambda: True)
|
|
writes: list[bool] = []
|
|
monkeypatch.setattr(flg, "write_github_login_deferred", writes.append)
|
|
|
|
flg.clear_github_login_deferral()
|
|
|
|
assert writes == [False]
|
|
|
|
|
|
def test_sleep_until_or_cancel_raises_on_escape(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(flg.os, "name", "posix")
|
|
monkeypatch.setattr(flg.sys.stdin, "isatty", lambda: True)
|
|
monkeypatch.setattr(flg.sys.stdin, "fileno", lambda: 0)
|
|
monkeypatch.setattr(
|
|
"surfaces.interactive_shell.ui.components.key_reader.read_key_unix",
|
|
lambda **_kwargs: "cancel",
|
|
)
|
|
monkeypatch.setattr("termios.tcgetattr", lambda _fd: [0] * 7)
|
|
monkeypatch.setattr("tty.setraw", lambda _fd: None)
|
|
restored: list[bool] = []
|
|
monkeypatch.setattr(
|
|
"termios.tcsetattr",
|
|
lambda _fd, _when, _attrs: restored.append(True),
|
|
)
|
|
|
|
def _ready(
|
|
_fd: int, *_args: object, **_kwargs: object
|
|
) -> tuple[list[int], list[int], list[int]]:
|
|
return ([0], [], [])
|
|
|
|
monkeypatch.setattr("select.select", _ready)
|
|
|
|
with pytest.raises(KeyboardInterrupt):
|
|
flg._sleep_until_or_cancel(1.0)
|
|
|
|
assert restored == [True]
|