All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 19s
Shared readiness wait and thin ERROR:UI_NOT_READY / BROWSER_START_TIMEOUT / ENV_DEGRADED codes so skills can separate slow machines from config failures. Optional browser launch timeout and read-only power/throttle diagnostics (non-blocking). Co-authored-by: Cursor <cursoragent@cursor.com>
191 lines
5.9 KiB
Python
191 lines
5.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""poll_until / 薄错误码 / browser launch timeout / machine_power 探测。"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from jiangchang_skill_core.machine_power import probe_machine_power
|
|
from jiangchang_skill_core.rpa import errors
|
|
from jiangchang_skill_core.rpa.browser import launch_persistent_browser
|
|
from jiangchang_skill_core.rpa.wait import poll_until, poll_until_sync
|
|
|
|
|
|
def test_poll_until_succeeds_when_predicate_becomes_true(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
sleeps: list[float] = []
|
|
|
|
async def _fake_sleep(seconds: float, *, chunk=None):
|
|
del chunk
|
|
sleeps.append(seconds)
|
|
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.rpa.wait.interruptible_sleep",
|
|
_fake_sleep,
|
|
)
|
|
state = {"n": 0}
|
|
|
|
def ready() -> bool:
|
|
state["n"] += 1
|
|
return state["n"] >= 3
|
|
|
|
result = asyncio.run(poll_until(ready, timeout_sec=2.0, interval_sec=0.1))
|
|
assert result.ok is True
|
|
assert result.timed_out is False
|
|
assert state["n"] == 3
|
|
assert sleeps
|
|
|
|
|
|
def test_poll_until_times_out(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
async def _fake_sleep(seconds: float, *, chunk=None):
|
|
del chunk, seconds
|
|
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.rpa.wait.interruptible_sleep",
|
|
_fake_sleep,
|
|
)
|
|
result = asyncio.run(poll_until(lambda: False, timeout_sec=0.01, interval_sec=0.01))
|
|
assert result.ok is False
|
|
assert result.timed_out is True
|
|
|
|
|
|
def test_poll_until_sync_ok(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.rpa.wait.interruptible_sleep_sync",
|
|
lambda seconds, *, chunk=None: None,
|
|
)
|
|
assert poll_until_sync(lambda: True, timeout_sec=1.0).ok is True
|
|
|
|
|
|
def test_thin_error_codes_exist() -> None:
|
|
assert errors.ERR_UI_NOT_READY == "ERROR:UI_NOT_READY"
|
|
assert errors.ERR_BROWSER_START_TIMEOUT == "ERROR:BROWSER_START_TIMEOUT"
|
|
assert errors.ERR_ENV_DEGRADED == "ERROR:ENV_DEGRADED"
|
|
assert errors.ERR_MISSING_BROWSER == "ERROR:MISSING_BROWSER"
|
|
|
|
|
|
def test_launch_persistent_browser_timeout_maps_code(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.rpa.browser.find_chrome_executable",
|
|
lambda: r"C:\fake\chrome.exe",
|
|
)
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.rpa.browser.persistent_context_launch_parts",
|
|
lambda extra_args=None: ([], None),
|
|
)
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.rpa.browser.stealth_enabled",
|
|
lambda: False,
|
|
)
|
|
|
|
async def _hang(**kwargs):
|
|
del kwargs
|
|
await asyncio.sleep(10)
|
|
raise AssertionError("should have been cancelled by wait_for")
|
|
|
|
playwright = MagicMock()
|
|
playwright.chromium.launch_persistent_context = _hang
|
|
|
|
async def _run():
|
|
with pytest.raises(RuntimeError) as ctx:
|
|
await launch_persistent_browser(
|
|
playwright,
|
|
profile_dir=r"C:\fake\profile",
|
|
timeout_ms=50,
|
|
)
|
|
assert str(ctx.value).startswith(errors.ERR_BROWSER_START_TIMEOUT)
|
|
|
|
asyncio.run(_run())
|
|
|
|
|
|
def test_launch_persistent_browser_missing_browser(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.rpa.browser.find_chrome_executable",
|
|
lambda: None,
|
|
)
|
|
|
|
async def _run():
|
|
with pytest.raises(RuntimeError) as ctx:
|
|
await launch_persistent_browser(MagicMock(), profile_dir=r"C:\fake\profile")
|
|
assert str(ctx.value).startswith(errors.ERR_MISSING_BROWSER)
|
|
|
|
asyncio.run(_run())
|
|
|
|
|
|
def test_probe_machine_power_shape_and_no_raise() -> None:
|
|
payload = probe_machine_power()
|
|
assert "probed" in payload
|
|
assert "ac_power" in payload
|
|
assert "nvidia" in payload
|
|
assert isinstance(payload["probed"], bool)
|
|
|
|
|
|
def test_collect_runtime_diagnostics_includes_machine_power_without_fatal(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
from jiangchang_skill_core import media_assets as ma
|
|
from jiangchang_skill_core.runtime_diagnostics import (
|
|
collect_runtime_diagnostics,
|
|
runtime_diagnostics_dict,
|
|
)
|
|
|
|
env = {"JIANGCHANG_DATA_ROOT": str(tmp_path / "data"), "OPENCLAW_RECORD_VIDEO": "0"}
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.runtime_diagnostics.probe_media_assets",
|
|
lambda *a, **k: ma.MediaAssetsStatus(
|
|
root=tmp_path,
|
|
exists=True,
|
|
ready=True,
|
|
source="local",
|
|
warnings=[],
|
|
ffmpeg_path=tmp_path / "ffmpeg.exe",
|
|
music_dir=tmp_path / "music",
|
|
music_ready=True,
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.runtime_diagnostics.probe_background_music",
|
|
lambda *a, **k: {
|
|
"music_root": str(tmp_path / "music"),
|
|
"mp3_count": 1,
|
|
"usable_count": 1,
|
|
"sample_path": str(tmp_path / "music" / "a.mp3"),
|
|
"issue": None,
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.runtime_diagnostics._platform_kit_version",
|
|
lambda: "1.2.11",
|
|
)
|
|
monkeypatch.setattr(
|
|
"jiangchang_skill_core.runtime_diagnostics.probe_machine_power",
|
|
lambda: {
|
|
"probed": True,
|
|
"ac_power": {
|
|
"ac_line_status": 1,
|
|
"on_ac_power": True,
|
|
"battery_life_percent": 100,
|
|
},
|
|
"nvidia": {
|
|
"gpu_name": "Fake",
|
|
"enforced_power_limit_w": 140.0,
|
|
"power_limit_w": 140.0,
|
|
"clocks_throttle_reasons_active": "None",
|
|
},
|
|
},
|
|
)
|
|
|
|
diag = collect_runtime_diagnostics(skill_slug="demo", env=env)
|
|
payload = runtime_diagnostics_dict(diag)
|
|
assert payload["machine_power"]["probed"] is True
|
|
assert diag.has_fatal_issues is False
|