优化代码

This commit is contained in:
2026-04-23 11:04:44 +08:00
parent d6ad90a7db
commit b9cd4dacec
10 changed files with 66 additions and 160 deletions

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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

View File

@@ -1 +1,4 @@
playwright>=1.42.0
[testing]
pytest>=7.0

View File

@@ -22,4 +22,4 @@ __all__ = [
"LaunchError",
"GatewayDownError",
]
__version__ = "0.4.0"
__version__ = "0.5.0"

View File

@@ -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",
]

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}