优化代码
This commit is contained in:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jiangchang-desktop-sdk"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
description = "匠厂桌面应用自动化测试 SDK + 共享 pytest plugin"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["playwright>=1.42.0"]
|
||||
@@ -12,12 +12,15 @@ dependencies = ["playwright>=1.42.0"]
|
||||
[project.optional-dependencies]
|
||||
testing = ["pytest>=7.0"]
|
||||
|
||||
# 让 pytest 在 pip install 后自动发现并加载 jiangchang_desktop_sdk.testing.plugin。
|
||||
# 未 pip install 时(开发态 sys.path 注入),skill 的 conftest 可改用
|
||||
# pytest_plugins = ["jiangchang_desktop_sdk.testing.plugin"]
|
||||
# 二者等价。
|
||||
[project.entry-points.pytest11]
|
||||
jiangchang_desktop_sdk_testing = "jiangchang_desktop_sdk.testing.plugin"
|
||||
# 注意:本 SDK **故意不注册 `[project.entry-points.pytest11]`**。
|
||||
#
|
||||
# plugin 的加载**单一入口**是每个 skill 的 `tests/desktop/conftest.py`,
|
||||
# 通过显式 `pytest_plugins = ["jiangchang_desktop_sdk.testing.plugin"]` 声明。
|
||||
# 这样做的好处:
|
||||
# - 保证不管 SDK 是 `pip install -e` 装的、还是仅通过 sys.path 注入的,
|
||||
# plugin 的注册方式完全一致;
|
||||
# - 避免 entry-point 与 `pytest_plugins` 同时生效导致 pluggy 抛
|
||||
# `Plugin already registered under a different name`。
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: jiangchang-desktop-sdk
|
||||
Version: 0.1.0
|
||||
Summary: 匠厂桌面应用自动化测试 SDK
|
||||
Version: 0.4.1
|
||||
Summary: 匠厂桌面应用自动化测试 SDK + 共享 pytest plugin
|
||||
Requires-Python: >=3.10
|
||||
Requires-Dist: playwright>=1.42.0
|
||||
Provides-Extra: testing
|
||||
Requires-Dist: pytest>=7.0; extra == "testing"
|
||||
|
||||
@@ -8,4 +8,10 @@ src/jiangchang_desktop_sdk.egg-info/SOURCES.txt
|
||||
src/jiangchang_desktop_sdk.egg-info/dependency_links.txt
|
||||
src/jiangchang_desktop_sdk.egg-info/requires.txt
|
||||
src/jiangchang_desktop_sdk.egg-info/top_level.txt
|
||||
src/jiangchang_desktop_sdk/testing/__init__.py
|
||||
src/jiangchang_desktop_sdk/testing/config.py
|
||||
src/jiangchang_desktop_sdk/testing/healthcheck.py
|
||||
src/jiangchang_desktop_sdk/testing/host_api.py
|
||||
src/jiangchang_desktop_sdk/testing/plugin.py
|
||||
src/jiangchang_desktop_sdk/testing/session_cleanup.py
|
||||
tests/test_client.py
|
||||
@@ -1 +1,4 @@
|
||||
playwright>=1.42.0
|
||||
|
||||
[testing]
|
||||
pytest>=7.0
|
||||
|
||||
@@ -22,4 +22,4 @@ __all__ = [
|
||||
"LaunchError",
|
||||
"GatewayDownError",
|
||||
]
|
||||
__version__ = "0.4.0"
|
||||
__version__ = "0.5.0"
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
- ``SkillInfo`` / ``discover_skill_root`` / ``parse_skill_md``
|
||||
- ``skill_healthcheck``
|
||||
- ``HostAPIClient`` / ``HostAPIError``
|
||||
- ``clear_main_agent_history``
|
||||
|
||||
pytest plugin 本体位于子模块 ``plugin``,由 pyproject.toml 的
|
||||
``[project.entry-points.pytest11]`` 自动注册;未 pip install 场景下,
|
||||
skill 的 conftest 也可通过 ``pytest_plugins`` 变量手动加载。
|
||||
**不提供任何会话清理能力**:测试完全模拟真实用户的 UI 操作,
|
||||
真实用户从不会去后台删会话,测试也不该走底层 API / 文件系统去清。
|
||||
|
||||
pytest plugin 本体位于子模块 ``plugin``,由每个 skill 的
|
||||
``tests/desktop/conftest.py`` 通过 ``pytest_plugins`` 变量显式加载。
|
||||
"""
|
||||
from .config import SkillInfo, discover_skill_root, parse_skill_md
|
||||
from .healthcheck import HealthCheckError, skill_healthcheck
|
||||
from .host_api import HostAPIClient, HostAPIError
|
||||
from .session_cleanup import clear_main_agent_history
|
||||
|
||||
__all__ = [
|
||||
"SkillInfo",
|
||||
@@ -24,5 +24,4 @@ __all__ = [
|
||||
"skill_healthcheck",
|
||||
"HostAPIClient",
|
||||
"HostAPIError",
|
||||
"clear_main_agent_history",
|
||||
]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""匠厂 Host HTTP API 的 Python 轻封装。
|
||||
|
||||
使用 stdlib urllib,避免为 SDK 引入新运行时依赖。只暴露本模块当前
|
||||
真正需要的端点;其余接口按需添加。
|
||||
使用 stdlib urllib,避免为 SDK 引入新运行时依赖。当前只暴露本模块
|
||||
真正需要的端点(健康探活、agent 列表),**不包含任何会话删除/会话
|
||||
文件系统操作**——测试侧要保持"只通过 UI 操作,不走底层接口"的原则,
|
||||
后续如需访问其他 host-api 端点请在此处按需添加读型方法。
|
||||
|
||||
Host API 默认端口:`13210`(见 jiangchang/electron/utils/config.ts 中 `CLAWX_HOST_API`)。
|
||||
可通过环境变量 ``CLAWX_PORT_CLAWX_HOST_API`` 或本类的 ``port`` 参数覆盖。
|
||||
@@ -42,8 +44,6 @@ class HostAPIClient:
|
||||
def base_url(self) -> str:
|
||||
return f"http://{self.host}:{self.port}"
|
||||
|
||||
# ── HTTP helpers ─────────────────────────────────────────
|
||||
|
||||
def _request(self, method: str, path: str, body: Any = None) -> Any:
|
||||
url = f"{self.base_url}{path}"
|
||||
data = None
|
||||
@@ -71,8 +71,6 @@ class HostAPIClient:
|
||||
except urllib.error.URLError as exc:
|
||||
raise HostAPIError(f"{method} {path} -> {exc}") from exc
|
||||
|
||||
# ── High-level API ───────────────────────────────────────
|
||||
|
||||
def ping(self) -> bool:
|
||||
try:
|
||||
self.list_agents()
|
||||
@@ -82,30 +80,3 @@ class HostAPIClient:
|
||||
|
||||
def list_agents(self) -> Dict[str, Any]:
|
||||
return self._request("GET", "/api/agents") or {}
|
||||
|
||||
def get_main_session_key(self) -> str:
|
||||
"""返回主 Agent 的 mainSessionKey(形如 ``agent:main:{mainKey}``)。
|
||||
|
||||
若多种形式都取不到,退化为 ``agent:main:default``——这通常仍能被
|
||||
后端 ``POST /api/sessions/delete`` 正确识别(会走 sessions.json 查找)。
|
||||
"""
|
||||
snapshot = self.list_agents()
|
||||
entries = snapshot.get("entries") or []
|
||||
for entry in entries:
|
||||
if entry.get("id") == "main":
|
||||
key = entry.get("mainSessionKey") or entry.get("sessionKey")
|
||||
if key:
|
||||
return key
|
||||
key = snapshot.get("mainSessionKey")
|
||||
if key:
|
||||
return key
|
||||
return "agent:main:default"
|
||||
|
||||
def delete_session(self, session_key: str) -> None:
|
||||
self._request("POST", "/api/sessions/delete", {"sessionKey": session_key})
|
||||
|
||||
def sessions_dir(self, agent_id: str = "main") -> str:
|
||||
"""返回指定 agent 的 sessions 目录绝对路径(供文件系统回退使用)。"""
|
||||
return os.path.join(
|
||||
os.path.expanduser("~"), ".openclaw", "agents", agent_id, "sessions"
|
||||
)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""jiangchang_desktop_sdk.testing 的 pytest plugin。
|
||||
|
||||
**设计原则:测试只模拟真实用户的 UI 操作,不做任何会话删除/底层清理**。
|
||||
真实用户装完匠厂 → 装技能 → 新建任务使用,从不会走后台接口去清理历史
|
||||
会话。测试如果走 API/FS 清理,就会与真实用户行为产生偏差,且历史上
|
||||
因"文件系统裸 rename 但不改 sessions.json"引发过孤儿数据 → 启动 RPC
|
||||
超时的 bug,所以此处**不提供任何会话清理能力**。
|
||||
|
||||
提供的 fixture / hook(所有 skill 通用):
|
||||
|
||||
session 级:
|
||||
- ``skill_info`` 当前 skill 的 SkillInfo(从 SKILL.md 解析)
|
||||
- ``host_api`` 匠厂 Host API 的 Python 客户端
|
||||
- ``jc_desktop_session_setup`` (autouse) 跑一次健康检查 + 清空主 Agent 历史
|
||||
- ``host_api`` 匠厂 Host API 的 Python 客户端(只读端点)
|
||||
- ``jc_desktop_session_setup`` (autouse) 跑一次技能健康检查
|
||||
|
||||
function 级:
|
||||
- ``failure_snapshot_dir`` 失败现场落盘目录(tests/desktop/artifacts/)
|
||||
@@ -18,13 +24,11 @@ hook:
|
||||
环境变量:
|
||||
- ``JIANGCHANG_E2E_SKILL_ROOT`` 显式指定 skill 根目录
|
||||
- ``JIANGCHANG_E2E_SKIP_HEALTHCHECK`` 置 1 可临时跳过健康检查
|
||||
- ``JIANGCHANG_E2E_SKIP_CLEAR_HISTORY`` 置 1 可临时跳过清空历史
|
||||
|
||||
加载方式(二选一):
|
||||
A) ``pip install -e`` 安装本 SDK 后,通过 ``pyproject.toml`` 的
|
||||
``[project.entry-points.pytest11]`` 自动加载;
|
||||
B) 在 skill 的 ``tests/desktop/conftest.py`` 里声明
|
||||
``pytest_plugins = ["jiangchang_desktop_sdk.testing.plugin"]``。
|
||||
加载方式:由每个 skill 的 ``tests/desktop/conftest.py`` 显式声明
|
||||
``pytest_plugins = ["jiangchang_desktop_sdk.testing.plugin"]`` 注册。
|
||||
本 SDK 的 ``pyproject.toml`` 故意不注册 ``[project.entry-points.pytest11]``,
|
||||
避免 entry-point 与 ``pytest_plugins`` 同时生效导致 pluggy 双注册冲突。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -36,13 +40,10 @@ import pytest
|
||||
from .config import SkillInfo, discover_skill_root, parse_skill_md
|
||||
from .healthcheck import HealthCheckError, skill_healthcheck
|
||||
from .host_api import HostAPIClient
|
||||
from .session_cleanup import clear_main_agent_history
|
||||
|
||||
logger = logging.getLogger("jiangchang-desktop-sdk.plugin")
|
||||
|
||||
|
||||
# ── Fixtures ──────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def skill_info(request) -> SkillInfo:
|
||||
rootpath = str(request.config.rootpath)
|
||||
@@ -60,35 +61,29 @@ def host_api() -> HostAPIClient:
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def jc_desktop_session_setup(skill_info, host_api):
|
||||
def jc_desktop_session_setup(skill_info):
|
||||
"""Desktop 测试 session 级一次性准备。
|
||||
|
||||
1. ``skill_healthcheck``:SKILL.md + ``python scripts/main.py health``,
|
||||
失败时 **fail-fast** 终止整个 session;
|
||||
2. ``clear_main_agent_history``:通过 Host API 清空主 Agent 的当前 session,
|
||||
规避跨 case 的上下文污染。API 不可达则回退文件系统,不影响测试启动。
|
||||
当前只做**技能健康检查**(SKILL.md 结构 + ``python scripts/main.py health``
|
||||
CLI 自检),失败时 fail-fast 终止整个 session。
|
||||
|
||||
**不做清空历史会话**——测试完全模拟真实用户的 UI 操作,真实用户从不
|
||||
会定期清会话,测试也不该清。历史积累带来的性能/容量问题应由匠厂侧
|
||||
解决,不在测试 SDK 的职责范围内。
|
||||
"""
|
||||
if not os.environ.get("JIANGCHANG_E2E_SKIP_HEALTHCHECK"):
|
||||
try:
|
||||
skill_healthcheck(skill_info)
|
||||
except HealthCheckError as exc:
|
||||
pytest.exit(
|
||||
f"[jc-healthcheck] 技能健康检查失败,终止测试 session:\n{exc}\n"
|
||||
"临时跳过:设置 JIANGCHANG_E2E_SKIP_HEALTHCHECK=1。",
|
||||
returncode=5,
|
||||
)
|
||||
else:
|
||||
if os.environ.get("JIANGCHANG_E2E_SKIP_HEALTHCHECK"):
|
||||
logger.warning("[jc-healthcheck] 已通过环境变量跳过健康检查")
|
||||
yield
|
||||
return
|
||||
|
||||
if not os.environ.get("JIANGCHANG_E2E_SKIP_CLEAR_HISTORY"):
|
||||
try:
|
||||
cleared = clear_main_agent_history(host_api)
|
||||
logger.info("[jc-cleanup] session 启动前已清空 %d 个历史 session", cleared)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("[jc-cleanup] 清空历史失败(非致命):%s", exc)
|
||||
else:
|
||||
logger.warning("[jc-cleanup] 已通过环境变量跳过清空历史")
|
||||
|
||||
try:
|
||||
skill_healthcheck(skill_info)
|
||||
except HealthCheckError as exc:
|
||||
pytest.exit(
|
||||
f"[jc-healthcheck] 技能健康检查失败,终止测试 session:\n{exc}\n"
|
||||
"临时跳过:设置 JIANGCHANG_E2E_SKIP_HEALTHCHECK=1。",
|
||||
returncode=5,
|
||||
)
|
||||
yield
|
||||
|
||||
|
||||
@@ -101,8 +96,6 @@ def failure_snapshot_dir(request) -> str:
|
||||
return artifacts
|
||||
|
||||
|
||||
# ── Hooks ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""清空主 Agent 的 chat 会话历史。
|
||||
|
||||
主路径:`POST /api/sessions/delete`(匠厂官方 Host API,软删除,改名 `.deleted.jsonl`)。
|
||||
回退路径:直接把 `~/.openclaw/agents/main/sessions/*.jsonl` 重命名为 `.deleted.jsonl`。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from .host_api import HostAPIClient, HostAPIError
|
||||
|
||||
logger = logging.getLogger("jiangchang-desktop-sdk.session_cleanup")
|
||||
|
||||
|
||||
def clear_main_agent_history(
|
||||
api: Optional[HostAPIClient] = None,
|
||||
*,
|
||||
agent_id: str = "main",
|
||||
strict: bool = False,
|
||||
) -> int:
|
||||
"""清空主 Agent 的当前 session。返回被删除的 session 数量。
|
||||
|
||||
``strict=True`` 时 API 失败立刻抛出;默认 ``False`` 下 API 失败会降级到
|
||||
文件系统批量重命名,尽最大努力保证下一个测试看到的是干净环境。
|
||||
"""
|
||||
api = api or HostAPIClient()
|
||||
|
||||
# ── Path A: Host API ───────────────────────────────────
|
||||
try:
|
||||
session_key = api.get_main_session_key()
|
||||
logger.debug("clear_main_agent_history: 主 session key = %s", session_key)
|
||||
api.delete_session(session_key)
|
||||
logger.info(
|
||||
"clear_main_agent_history: HTTP API 已软删除 %s", session_key
|
||||
)
|
||||
return 1
|
||||
except HostAPIError as exc:
|
||||
if strict:
|
||||
raise
|
||||
logger.warning(
|
||||
"clear_main_agent_history: HTTP API 失败(%s),回退到文件系统", exc
|
||||
)
|
||||
|
||||
# ── Path B: 文件系统回退 ────────────────────────────────
|
||||
sessions_dir = api.sessions_dir(agent_id)
|
||||
if not os.path.isdir(sessions_dir):
|
||||
logger.info(
|
||||
"clear_main_agent_history: %s 不存在,无历史可清", sessions_dir
|
||||
)
|
||||
return 0
|
||||
|
||||
deleted = 0
|
||||
for name in os.listdir(sessions_dir):
|
||||
if not name.endswith(".jsonl") or name.endswith(".deleted.jsonl"):
|
||||
continue
|
||||
src = os.path.join(sessions_dir, name)
|
||||
dst = src[: -len(".jsonl")] + ".deleted.jsonl"
|
||||
try:
|
||||
os.replace(src, dst)
|
||||
deleted += 1
|
||||
except OSError as exc:
|
||||
logger.warning(
|
||||
"clear_main_agent_history: 重命名 %s 失败:%s", src, exc
|
||||
)
|
||||
if strict:
|
||||
raise
|
||||
logger.info(
|
||||
"clear_main_agent_history: 文件系统回退共软删除 %d 个 session", deleted
|
||||
)
|
||||
return deleted
|
||||
@@ -46,7 +46,9 @@ $ErrorActionPreference = "Stop"
|
||||
function Invoke-Git {
|
||||
param([Parameter(Mandatory = $true)][string]$Args)
|
||||
Write-Host ">> git $Args" -ForegroundColor DarkGray
|
||||
& cmd /c "git $Args"
|
||||
# 将 git 的 stderr 合并进 stdout,避免 PowerShell 在 $ErrorActionPreference=Stop 下
|
||||
# 把 "remote: ..." / "warning: LF -> CRLF" 等非错误输出误判为异常。
|
||||
& cmd /c "git $Args 2>&1"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git command failed: git $Args"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user