From b9cd4dacec50f5c6a6f1c647941d232d8507a176 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Thu, 23 Apr 2026 11:04:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/jiangchang-desktop-sdk/pyproject.toml | 17 +++-- .../jiangchang_desktop_sdk.egg-info/PKG-INFO | 6 +- .../SOURCES.txt | 6 ++ .../requires.txt | 3 + .../src/jiangchang_desktop_sdk/__init__.py | 2 +- .../testing/__init__.py | 11 ++- .../testing/host_api.py | 37 +--------- .../jiangchang_desktop_sdk/testing/plugin.py | 67 ++++++++--------- .../testing/session_cleanup.py | 73 ------------------- tools/release.ps1 | 4 +- 10 files changed, 66 insertions(+), 160 deletions(-) delete mode 100644 sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/session_cleanup.py diff --git a/sdk/jiangchang-desktop-sdk/pyproject.toml b/sdk/jiangchang-desktop-sdk/pyproject.toml index 0788a52..db96dad 100644 --- a/sdk/jiangchang-desktop-sdk/pyproject.toml +++ b/sdk/jiangchang-desktop-sdk/pyproject.toml @@ -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"] diff --git a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/PKG-INFO b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/PKG-INFO index 3d3ed12..7a5e1f5 100644 --- a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/PKG-INFO +++ b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/PKG-INFO @@ -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" diff --git a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/SOURCES.txt b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/SOURCES.txt index aa9f444..5e18b0c 100644 --- a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/SOURCES.txt +++ b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/SOURCES.txt @@ -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 \ No newline at end of file diff --git a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/requires.txt b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/requires.txt index 1299365..cd72f7c 100644 --- a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/requires.txt +++ b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk.egg-info/requires.txt @@ -1 +1,4 @@ playwright>=1.42.0 + +[testing] +pytest>=7.0 diff --git a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/__init__.py b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/__init__.py index 64b2aef..009c382 100644 --- a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/__init__.py +++ b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/__init__.py @@ -22,4 +22,4 @@ __all__ = [ "LaunchError", "GatewayDownError", ] -__version__ = "0.4.0" +__version__ = "0.5.0" diff --git a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/__init__.py b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/__init__.py index eb2cf69..8ca44b0 100644 --- a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/__init__.py +++ b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/__init__.py @@ -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", ] diff --git a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/host_api.py b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/host_api.py index fc10b14..0936424 100644 --- a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/host_api.py +++ b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/host_api.py @@ -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" - ) diff --git a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/plugin.py b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/plugin.py index 978a6ba..156ae14 100644 --- a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/plugin.py +++ b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/plugin.py @@ -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 diff --git a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/session_cleanup.py b/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/session_cleanup.py deleted file mode 100644 index 827c1ba..0000000 --- a/sdk/jiangchang-desktop-sdk/src/jiangchang_desktop_sdk/testing/session_cleanup.py +++ /dev/null @@ -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 diff --git a/tools/release.ps1 b/tools/release.ps1 index a11088c..eb3a481 100644 --- a/tools/release.ps1 +++ b/tools/release.ps1 @@ -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" }