docs(config): unify config.get reads and remove ALLOW_* gates
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
This commit is contained in:
@@ -17,8 +17,8 @@
|
|||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| **`mock`** | 纯内存或 fixture,**默认单测/CI** | 模板 `.env.example` 默认 `OPENCLAW_TEST_TARGET=mock` |
|
| **`mock`** | 纯内存或 fixture,**默认单测/CI** | 模板 `.env.example` 默认 `OPENCLAW_TEST_TARGET=mock` |
|
||||||
| **`simulator_rpa`** | 仿真站点或桌面仿真,可半集成 | 开发联调可选 |
|
| **`simulator_rpa`** | 仿真站点或桌面仿真,可半集成 | 开发联调可选 |
|
||||||
| **`real_api`** | 真实 API | **必须** `ALLOW_REAL_API=1` |
|
| **`real_api`** | 真实 API | 生产 / 集成测试显式设 `OPENCLAW_TEST_TARGET=real_api` |
|
||||||
| **`real_rpa`** | 真实浏览器/真实系统 | **必须** `ALLOW_REAL_RPA=1` |
|
| **`real_rpa`** | 真实浏览器/真实系统 | 生产 / 集成测试显式设 `OPENCLAW_TEST_TARGET=real_rpa` |
|
||||||
|
|
||||||
- **mock**:纯离线、不联网,给单测 / CI / 开发自测,**保证可重复**。
|
- **mock**:纯离线、不联网,给单测 / CI / 开发自测,**保证可重复**。
|
||||||
- **simulator_rpa**:操作仿真平台(如 `sandbox.jc2009.com`),跑端到端流程但不碰生产。
|
- **simulator_rpa**:操作仿真平台(如 `sandbox.jc2009.com`),跑端到端流程但不碰生产。
|
||||||
@@ -27,17 +27,7 @@
|
|||||||
|
|
||||||
> 推荐优先级:**real_api > simulator_rpa > real_rpa**,mock 永远保留做 CI。
|
> 推荐优先级:**real_api > simulator_rpa > real_rpa**,mock 永远保留做 CI。
|
||||||
|
|
||||||
## 权限开关
|
配置读取见 `CONFIG.md`:**bootstrap 之后业务代码只通过 `config.get*()` 读 `OPENCLAW_TEST_TARGET` 等项**(进程 env > 用户 `.env` > `.env.example`)。
|
||||||
|
|
||||||
未显式授权时**不得**落到真实 API/RPA:
|
|
||||||
|
|
||||||
| 变量 | 作用 |
|
|
||||||
|------|------|
|
|
||||||
| `ALLOW_REAL_API=1` | 允许 `real_api` 访问真实 HTTP |
|
|
||||||
| `ALLOW_REAL_RPA=1` | 允许 `real_rpa` 驱动真实浏览器/RPA |
|
|
||||||
| `ALLOW_WRITE_ACTIONS=1` | 在 `real_*` 下允许**写**操作(提交表单、下单等) |
|
|
||||||
|
|
||||||
进程环境变量优先于用户 `.env`(见 `CONFIG.md` 三层优先级)。
|
|
||||||
|
|
||||||
## 目录骨架
|
## 目录骨架
|
||||||
|
|
||||||
@@ -74,8 +64,10 @@ scripts/service/<domain>_adapter/
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# __init__.py
|
# __init__.py
|
||||||
|
from jiangchang_skill_core import config
|
||||||
|
|
||||||
def get_adapter():
|
def get_adapter():
|
||||||
target = (os.environ.get("OPENCLAW_TEST_TARGET") or "mock").lower()
|
target = (config.get("OPENCLAW_TEST_TARGET") or "mock").lower()
|
||||||
if target in ("unit", "mock"):
|
if target in ("unit", "mock"):
|
||||||
return MockAdapter()
|
return MockAdapter()
|
||||||
if target == "real_api":
|
if target == "real_api":
|
||||||
@@ -85,7 +77,7 @@ def get_adapter():
|
|||||||
return SimRpaAdapter()
|
return SimRpaAdapter()
|
||||||
```
|
```
|
||||||
|
|
||||||
`get_adapter()` 实现应配合 `tests/adapter_test_utils.py` 的 profile 策略:**未授权时不 silently 开启真实网络/RPA**。
|
`get_adapter()` 实现应配合 `tests/adapter_test_utils.py` 的 profile 策略:**默认必跑测试不得误设 `OPENCLAW_TEST_TARGET=real_*`**。
|
||||||
|
|
||||||
## contract tests
|
## contract tests
|
||||||
|
|
||||||
|
|||||||
@@ -71,22 +71,14 @@ def test_whatever():
|
|||||||
| `mock` | 与 `unit` 类似的安全档位(显式语义) |
|
| `mock` | 与 `unit` 类似的安全档位(显式语义) |
|
||||||
| `simulator_api` | 仅允许 **仿真 HTTP** 类集成(如 localhost) |
|
| `simulator_api` | 仅允许 **仿真 HTTP** 类集成(如 localhost) |
|
||||||
| `simulator_rpa` | 仅允许 **仿真页面 / 录播 RPA** |
|
| `simulator_rpa` | 仅允许 **仿真页面 / 录播 RPA** |
|
||||||
| `real_api` | 真实 API(另需 `ALLOW_REAL_API=1`) |
|
| `real_api` | 真实 API(显式设 `OPENCLAW_TEST_TARGET=real_api`) |
|
||||||
| `real_rpa` | 真实 RPA(另需 `ALLOW_REAL_RPA=1`) |
|
| `real_rpa` | 真实 RPA(显式设 `OPENCLAW_TEST_TARGET=real_rpa`) |
|
||||||
|
|
||||||
未设置环境变量 ⇒ 等价 `unit`。
|
未设置环境变量 ⇒ 等价 `unit`。
|
||||||
|
|
||||||
授权开关(显式 `1`)语义 **`ALLOW_REAL_API` / `ALLOW_REAL_RPA` / `ALLOW_WRITE_ACTIONS`**——摘录 [`tests/README.md`](../tests/README.md) §5.2:
|
默认策略摘要:**不要在 unittest 必跑路径误设 `OPENCLAW_TEST_TARGET=real_*`**。
|
||||||
|
|
||||||
| 变量 | 作用 |
|
档位读取与业务代码一致:经 `jiangchang_skill_core.config.get("OPENCLAW_TEST_TARGET")`(见 `CONFIG.md`)。
|
||||||
|------|------|
|
|
||||||
| `ALLOW_REAL_API=1` | 允许 `real_api` profile 访问真实 HTTP 通道 |
|
|
||||||
| `ALLOW_REAL_RPA=1` | 允许 `real_rpa` profile 驱动真实浏览器/RPA |
|
|
||||||
| `ALLOW_WRITE_ACTIONS=1` | 在 `real_*` 下允许**写**操作(提交表单、下单等) |
|
|
||||||
|
|
||||||
默认策略摘要见 §5.3:**不要在 unittest 必跑路径误把闸门打开**。
|
|
||||||
|
|
||||||
兼容别名:`OPENCLOW_TEST_TARGET`(历史拼写)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -164,7 +156,7 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
|
|||||||
| **零硬编码凭证** | token / cookie / 生产 URL → 用虚构域名或 vault ref |
|
| **零硬编码凭证** | token / cookie / 生产 URL → 用虚构域名或 vault ref |
|
||||||
| mock 优先 | 逻辑应在 service + FakeAdapter,而不是巨胖 CLI 断言 |
|
| mock 优先 | 逻辑应在 service + FakeAdapter,而不是巨胖 CLI 断言 |
|
||||||
| 结构化错误 | 断言错误码字段,而不是 substring of stderr 漂移集合 |
|
| 结构化错误 | 断言错误码字段,而不是 substring of stderr 漂移集合 |
|
||||||
| **integration = 显式开关** | `OPENCLAW_TEST_TARGET` + `ALLOW_REAL_*` / `ALLOW_WRITE_ACTIONS` |
|
| **integration = 显式档位** | 仅 `OPENCLAW_TEST_TARGET`(`real_*` 只放 integration / 手动触发) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from jiangchang_skill_core import config
|
||||||
|
|
||||||
from service.adapter.base import BatchAdapterBase, BatchItem, BatchSubmitResult
|
from service.adapter.base import BatchAdapterBase, BatchItem, BatchSubmitResult
|
||||||
from service.adapter.mock import MockBatchAdapter
|
from service.adapter.mock import MockBatchAdapter
|
||||||
from service.adapter.simulator_rpa import SimulatorBrowserRpaAdapter
|
from service.adapter.simulator_rpa import SimulatorBrowserRpaAdapter
|
||||||
@@ -23,7 +24,7 @@ __all__ = [
|
|||||||
|
|
||||||
|
|
||||||
def select_adapter(artifacts_dir: Optional[str] = None) -> BatchAdapterBase:
|
def select_adapter(artifacts_dir: Optional[str] = None) -> BatchAdapterBase:
|
||||||
target = os.environ.get("OPENCLAW_TEST_TARGET", "").strip().lower()
|
target = (config.get("OPENCLAW_TEST_TARGET") or "").strip().lower()
|
||||||
|
|
||||||
if target in ("mock", "unit"):
|
if target in ("mock", "unit"):
|
||||||
logger.info("target '%s': MockBatchAdapter", target)
|
logger.info("target '%s': MockBatchAdapter", target)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import re
|
|||||||
import time
|
import time
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from jiangchang_skill_core import config
|
||||||
|
|
||||||
from service.adapter.base import BatchAdapterBase, BatchItem, BatchSubmitResult
|
from service.adapter.base import BatchAdapterBase, BatchItem, BatchSubmitResult
|
||||||
from service.browser_session import browser_session, find_chrome_executable
|
from service.browser_session import browser_session, find_chrome_executable
|
||||||
from util.constants import (
|
from util.constants import (
|
||||||
@@ -45,8 +47,7 @@ class SimulatorBrowserRpaAdapter(BatchAdapterBase):
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.base_url = (base_url or resolve_simulator_base_url()).rstrip("/")
|
self.base_url = (base_url or resolve_simulator_base_url()).rstrip("/")
|
||||||
if headless is None:
|
if headless is None:
|
||||||
env = os.environ.get("OPENCLAW_BROWSER_HEADLESS", "").strip().lower()
|
self.headless = config.get_bool("OPENCLAW_BROWSER_HEADLESS", default=False)
|
||||||
self.headless = env in ("1", "true", "yes")
|
|
||||||
else:
|
else:
|
||||||
self.headless = headless
|
self.headless = headless
|
||||||
self.artifacts_dir = artifacts_dir
|
self.artifacts_dir = artifacts_dir
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# skill-template 测试说明(企业数字员工技能)
|
# skill-template 测试说明(企业数字员工技能)
|
||||||
|
|
||||||
本目录提供面向 **ERP / TMS / WMS / 船司 / 报关 / 邮件 / 浏览器 / LLM / RPA** 等外联场景的**分层测试骨架**:默认仅内存与本地 SQLite;**真实 API / 真实 RPA** 必须通过环境变量显式授权,且仅以 ``*.sample`` 或复制后的文件提供范式。
|
本目录提供面向 **ERP / TMS / WMS / 船司 / 报关 / 邮件 / 浏览器 / LLM / RPA** 等外联场景的**分层测试骨架**:默认仅内存与本地 SQLite;**真实 API / 真实 RPA** 必须显式设置 ``OPENCLAW_TEST_TARGET``,且仅以 ``*.sample`` 或复制后的文件提供范式。
|
||||||
|
|
||||||
复制为新技能后,请保留隔离数据根与 profile 策略,再按域扩展用例。
|
复制为新技能后,请保留隔离数据根与 profile 策略,再按域扩展用例。
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
| 纯函数 / 业务规则 | `tests/test_*.py` | 是 | 不依赖真实外部系统 |
|
| 纯函数 / 业务规则 | `tests/test_*.py` | 是 | 不依赖真实外部系统 |
|
||||||
| service 层契约 | 从 `tests/samples/test_service_contract.py.sample` 复制到 `tests/test_service_contract.py` | 是,复制后 | 用 `FakeAdapter` / stub 注入外部依赖 |
|
| service 层契约 | 从 `tests/samples/test_service_contract.py.sample` 复制到 `tests/test_service_contract.py` | 是,复制后 | 用 `FakeAdapter` / stub 注入外部依赖 |
|
||||||
| golden fixture 回归 | 从 `tests/samples/test_golden_cases.py.sample` 复制到 `tests/test_golden_cases.py` | 是,复制后 | 输入样例 + 期望输出,适合解析、计算、校验类技能 |
|
| golden fixture 回归 | 从 `tests/samples/test_golden_cases.py.sample` 复制到 `tests/test_golden_cases.py` | 是,复制后 | 输入样例 + 期望输出,适合解析、计算、校验类技能 |
|
||||||
| 仿真 API | `tests/integration/test_simulator_api_contract.py.sample` | 否 | 复制并设置 `OPENCLAW_TEST_TARGET=simulator_api`(或兼容键名 `OPENCLOW_TEST_TARGET`)后按需运行 |
|
| 仿真 API | `tests/integration/test_simulator_api_contract.py.sample` | 否 | 复制并设置 `OPENCLAW_TEST_TARGET=simulator_api` 后按需运行 |
|
||||||
| 仿真 RPA / 页面操作 | `tests/integration/test_simulator_rpa_contract.py.sample` | 否 | 用 localhost / 仿真页面,不碰真实系统 |
|
| 仿真 RPA / 页面操作 | `tests/integration/test_simulator_rpa_contract.py.sample` | 否 | 用 localhost / 仿真页面,不碰真实系统 |
|
||||||
| 真实 API | `tests/integration/test_real_api_contract.py.sample` | 否 | 必须 `OPENCLAW_TEST_TARGET=real_api` 且 `ALLOW_REAL_API=1` |
|
| 真实 API | `tests/integration/test_real_api_contract.py.sample` | 否 | 必须 `OPENCLAW_TEST_TARGET=real_api` |
|
||||||
| 真实 RPA | `tests/integration/test_real_rpa_contract.py.sample` | 否 | 最高风险,只能手动触发 |
|
| 真实 RPA | `tests/integration/test_real_rpa_contract.py.sample` | 否 | 最高风险,只能手动触发 |
|
||||||
| 桌面宿主 E2E | `tests/desktop/test_desktop_smoke.py.sample` | 否 | 需要 pytest + `jiangchang_desktop_sdk` |
|
| 桌面宿主 E2E | `tests/desktop/test_desktop_smoke.py.sample` | 否 | 需要 pytest + `jiangchang_desktop_sdk` |
|
||||||
|
|
||||||
@@ -125,11 +125,11 @@ python tests/run_tests.py test_cli_smoke
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 环境变量:测试档位与授权
|
## 5. 环境变量:测试档位
|
||||||
|
|
||||||
### 5.1 测试档位(二选一变量名,后者为历史拼写兼容)
|
### 5.1 测试档位
|
||||||
|
|
||||||
- ``OPENCLAW_TEST_TARGET`` 或 ``OPENCLOW_TEST_TARGET``
|
- ``OPENCLAW_TEST_TARGET``
|
||||||
|
|
||||||
**合法取值**(非法值将使 ``get_test_target()`` 抛 ``ValueError``):
|
**合法取值**(非法值将使 ``get_test_target()`` 抛 ``ValueError``):
|
||||||
|
|
||||||
@@ -139,23 +139,15 @@ python tests/run_tests.py test_cli_smoke
|
|||||||
| ``mock`` | 与 ``unit`` 类似的安全档位(显式语义) |
|
| ``mock`` | 与 ``unit`` 类似的安全档位(显式语义) |
|
||||||
| ``simulator_api`` | 仅允许 **仿真 HTTP** 类集成(如 localhost) |
|
| ``simulator_api`` | 仅允许 **仿真 HTTP** 类集成(如 localhost) |
|
||||||
| ``simulator_rpa`` | 仅允许 **仿真页面 / 录播 RPA** |
|
| ``simulator_rpa`` | 仅允许 **仿真页面 / 录播 RPA** |
|
||||||
| ``real_api`` | 真实 API(另需 ``ALLOW_REAL_API=1``) |
|
| ``real_api`` | 真实 API(显式设 ``OPENCLAW_TEST_TARGET=real_api``) |
|
||||||
| ``real_rpa`` | 真实 RPA(另需 ``ALLOW_REAL_RPA=1``) |
|
| ``real_rpa`` | 真实 RPA(显式设 ``OPENCLAW_TEST_TARGET=real_rpa``) |
|
||||||
|
|
||||||
未设置时等价 ``unit``。
|
未设置时等价 ``unit``。
|
||||||
|
|
||||||
### 5.2 授权开关(显式 ``1``)
|
### 5.2 默认安全策略(``adapter_test_utils``)
|
||||||
|
|
||||||
| 变量 | 作用 |
|
|
||||||
|------|------|
|
|
||||||
| ``ALLOW_REAL_API=1`` | 允许 ``real_api`` profile 访问真实 HTTP 通道 |
|
|
||||||
| ``ALLOW_REAL_RPA=1`` | 允许 ``real_rpa`` profile 驱动真实浏览器/RPA |
|
|
||||||
| ``ALLOW_WRITE_ACTIONS=1`` | 在 ``real_*`` 下允许**写**操作(提交表单、下单等) |
|
|
||||||
|
|
||||||
### 5.3 默认安全策略(``adapter_test_utils``)
|
|
||||||
|
|
||||||
- 默认 ``unit``:不访问真实 API、不跑真实 RPA、不做写操作、不读取真实密钥、不写真实数据根。
|
- 默认 ``unit``:不访问真实 API、不跑真实 RPA、不做写操作、不读取真实密钥、不写真实数据根。
|
||||||
- ``assert_no_real_network_env()`` 断言默认未误开 ``ALLOW_REAL_*``(用于必跑用例自检)。
|
- ``assert_no_real_network_env()`` 断言默认未误设 ``OPENCLAW_TEST_TARGET`` 为 ``real_api`` / ``real_rpa``(用于必跑用例自检)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -170,7 +162,7 @@ python tests/run_tests.py test_cli_smoke
|
|||||||
- 默认测试只能使用 **mock、stub、`FakeAdapter`、fixtures、隔离 SQLite** 等安全手段。
|
- 默认测试只能使用 **mock、stub、`FakeAdapter`、fixtures、隔离 SQLite** 等安全手段。
|
||||||
- 业务逻辑应优先在 **service 层** 编写与测试,不要只断言 CLI 文案。
|
- 业务逻辑应优先在 **service 层** 编写与测试,不要只断言 CLI 文案。
|
||||||
- 异常路径应返回**结构化错误**(如统一错误码/字段),避免裸异常直接穿透到 CLI。
|
- 异常路径应返回**结构化错误**(如统一错误码/字段),避免裸异常直接穿透到 CLI。
|
||||||
- 有真实系统联调需求时,测试应写在 `tests/integration/`,并配合 `OPENCLAW_TEST_TARGET` / `ALLOW_REAL_*` / `ALLOW_WRITE_ACTIONS` 等**显式开关**;不得默认开启。
|
- 有真实系统联调需求时,测试应写在 `tests/integration/`,并显式设置 ``OPENCLAW_TEST_TARGET`` 为 ``real_api`` / ``real_rpa``;不得默认开启。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
企业技能对接外部系统(ERP / TMS / WMS / 船司 / 报关 / 邮件 / 浏览器 / LLM / RPA)时,
|
企业技能对接外部系统(ERP / TMS / WMS / 船司 / 报关 / 邮件 / 浏览器 / LLM / RPA)时,
|
||||||
测试侧共用的 **profile / 授权 / 禁止真实外呼** 工具。
|
测试侧共用的 **profile / 禁止真实外呼** 工具。
|
||||||
|
|
||||||
- 仅标准库;不发起 HTTP;不启动浏览器。
|
- 仅标准库 + jiangchang_skill_core.config;不发起 HTTP;不启动浏览器。
|
||||||
- 真实 API / RPA 契约测试请放在 ``tests/integration/*.sample``,并配合环境变量显式授权。
|
- 真实 API / RPA 契约测试请放在 ``tests/integration/*.sample``,并显式设置 ``OPENCLAW_TEST_TARGET``。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
# 允许的 OPENCLAW_TEST_TARGET / OPENCLOW_TEST_TARGET 取值(后者为历史拼写兼容)
|
from jiangchang_skill_core import config
|
||||||
|
|
||||||
ALLOWED_TEST_TARGETS: Final[frozenset[str]] = frozenset(
|
ALLOWED_TEST_TARGETS: Final[frozenset[str]] = frozenset(
|
||||||
{
|
{
|
||||||
"unit",
|
"unit",
|
||||||
@@ -25,12 +25,12 @@ ALLOWED_TEST_TARGETS: Final[frozenset[str]] = frozenset(
|
|||||||
|
|
||||||
|
|
||||||
def _raw_test_target_env() -> str:
|
def _raw_test_target_env() -> str:
|
||||||
return (os.environ.get("OPENCLAW_TEST_TARGET") or os.environ.get("OPENCLOW_TEST_TARGET") or "").strip()
|
return (config.get("OPENCLAW_TEST_TARGET") or "").strip()
|
||||||
|
|
||||||
|
|
||||||
def get_test_target() -> str:
|
def get_test_target() -> str:
|
||||||
"""
|
"""
|
||||||
读取 ``OPENCLAW_TEST_TARGET`` 或 ``OPENCLOW_TEST_TARGET``(兼容旧拼写)。
|
读取 ``OPENCLAW_TEST_TARGET``(经 ``config.get`` 三级合并)。
|
||||||
|
|
||||||
未设置时返回 ``unit``。非法取值抛出 ``ValueError``,便于尽早发现拼写错误。
|
未设置时返回 ``unit``。非法取值抛出 ``ValueError``,便于尽早发现拼写错误。
|
||||||
"""
|
"""
|
||||||
@@ -46,18 +46,15 @@ def get_test_target() -> str:
|
|||||||
|
|
||||||
def real_access_allowed(profile: str) -> bool:
|
def real_access_allowed(profile: str) -> bool:
|
||||||
"""
|
"""
|
||||||
判断当前环境是否允许对 ``profile`` 做「真实通道」访问(仍不代替业务鉴权)。
|
判断当前 ``OPENCLAW_TEST_TARGET`` 是否允许运行 ``profile`` 档位测试。
|
||||||
|
|
||||||
- ``real_api``:需要 ``ALLOW_REAL_API == "1"``。
|
|
||||||
- ``real_rpa``:需要 ``ALLOW_REAL_RPA == "1"``。
|
|
||||||
- 写操作另需 ``ALLOW_WRITE_ACTIONS == "1"``(由 ``should_skip_profile(..., write=True)`` 组合检查)。
|
|
||||||
"""
|
"""
|
||||||
if profile == "real_api":
|
target = get_test_target()
|
||||||
return os.environ.get("ALLOW_REAL_API", "") == "1"
|
|
||||||
if profile == "real_rpa":
|
|
||||||
return os.environ.get("ALLOW_REAL_RPA", "") == "1"
|
|
||||||
if profile in ("mock", "simulator_api", "simulator_rpa"):
|
if profile in ("mock", "simulator_api", "simulator_rpa"):
|
||||||
return True
|
return True
|
||||||
|
if profile == "real_api":
|
||||||
|
return target == "real_api"
|
||||||
|
if profile == "real_rpa":
|
||||||
|
return target == "real_rpa"
|
||||||
raise ValueError(f"unknown profile for real_access_allowed: {profile!r}")
|
raise ValueError(f"unknown profile for real_access_allowed: {profile!r}")
|
||||||
|
|
||||||
|
|
||||||
@@ -67,42 +64,19 @@ def should_skip_profile(profile: str, write: bool = False) -> tuple[bool, str]:
|
|||||||
|
|
||||||
策略摘要:
|
策略摘要:
|
||||||
- ``mock``:永不跳过(内存 / FakeAdapter,无外呼)。
|
- ``mock``:永不跳过(内存 / FakeAdapter,无外呼)。
|
||||||
- ``simulator_api``:仅当 ``get_test_target() == "simulator_api"`` 时不跳过。
|
- ``simulator_api`` / ``simulator_rpa``:仅当 ``get_test_target()`` 与 profile 一致时不跳过。
|
||||||
- ``simulator_rpa``:仅当 target 为 ``simulator_rpa`` 时不跳过。
|
- ``real_api`` / ``real_rpa``:仅当 ``get_test_target()`` 与 profile 一致时不跳过。
|
||||||
- ``real_api``:target 须为 ``real_api`` 且 ``ALLOW_REAL_API=1``;``write=True`` 还须 ``ALLOW_WRITE_ACTIONS=1``。
|
- ``write`` 参数保留以兼容 integration 样例签名;档位一致即允许写操作测试。
|
||||||
- ``real_rpa``:target 须为 ``real_rpa`` 且 ``ALLOW_REAL_RPA=1``;``write=True`` 还须 ``ALLOW_WRITE_ACTIONS=1``。
|
|
||||||
"""
|
"""
|
||||||
|
_ = write
|
||||||
target = get_test_target()
|
target = get_test_target()
|
||||||
|
|
||||||
if profile == "mock":
|
if profile == "mock":
|
||||||
return (False, "")
|
return (False, "")
|
||||||
|
|
||||||
if profile == "simulator_api":
|
if profile in ("simulator_api", "simulator_rpa", "real_api", "real_rpa"):
|
||||||
if target != "simulator_api":
|
if target != profile:
|
||||||
return (True, f"OPENCLAW_TEST_TARGET must be simulator_api (got {target!r})")
|
return (True, f"OPENCLAW_TEST_TARGET must be {profile} (got {target!r})")
|
||||||
return (False, "")
|
|
||||||
|
|
||||||
if profile == "simulator_rpa":
|
|
||||||
if target != "simulator_rpa":
|
|
||||||
return (True, f"OPENCLAW_TEST_TARGET must be simulator_rpa (got {target!r})")
|
|
||||||
return (False, "")
|
|
||||||
|
|
||||||
if profile == "real_api":
|
|
||||||
if target != "real_api":
|
|
||||||
return (True, f"OPENCLAW_TEST_TARGET must be real_api (got {target!r})")
|
|
||||||
if os.environ.get("ALLOW_REAL_API", "") != "1":
|
|
||||||
return (True, "ALLOW_REAL_API is not 1")
|
|
||||||
if write and os.environ.get("ALLOW_WRITE_ACTIONS", "") != "1":
|
|
||||||
return (True, "ALLOW_WRITE_ACTIONS is not 1 (write requested)")
|
|
||||||
return (False, "")
|
|
||||||
|
|
||||||
if profile == "real_rpa":
|
|
||||||
if target != "real_rpa":
|
|
||||||
return (True, f"OPENCLAW_TEST_TARGET must be real_rpa (got {target!r})")
|
|
||||||
if os.environ.get("ALLOW_REAL_RPA", "") != "1":
|
|
||||||
return (True, "ALLOW_REAL_RPA is not 1")
|
|
||||||
if write and os.environ.get("ALLOW_WRITE_ACTIONS", "") != "1":
|
|
||||||
return (True, "ALLOW_WRITE_ACTIONS is not 1 (write requested)")
|
|
||||||
return (False, "")
|
return (False, "")
|
||||||
|
|
||||||
raise ValueError(f"unknown profile: {profile!r}")
|
raise ValueError(f"unknown profile: {profile!r}")
|
||||||
@@ -134,9 +108,11 @@ class FakeAdapter:
|
|||||||
|
|
||||||
def assert_no_real_network_env() -> None:
|
def assert_no_real_network_env() -> None:
|
||||||
"""
|
"""
|
||||||
断言当前进程未显式打开「真实外呼」授权开关(默认安全 CI / 本地开发)。
|
断言默认必跑套件未误设真实外联档位(``real_api`` / ``real_rpa``)。
|
||||||
|
|
||||||
若需在受控环境跑真实集成,请显式设置 ``ALLOW_REAL_*`` 并使用 ``tests/integration/*.sample``。
|
若需在受控环境跑真实集成,请显式设置 ``OPENCLAW_TEST_TARGET`` 并使用 ``tests/integration/*.sample``。
|
||||||
"""
|
"""
|
||||||
assert os.environ.get("ALLOW_REAL_API", "") != "1", "ALLOW_REAL_API must not be 1 in default unit tests"
|
target = get_test_target()
|
||||||
assert os.environ.get("ALLOW_REAL_RPA", "") != "1", "ALLOW_REAL_RPA must not be 1 in default unit tests"
|
assert target not in ("real_api", "real_rpa"), (
|
||||||
|
f"OPENCLAW_TEST_TARGET must not be {target!r} in default unit tests"
|
||||||
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|------|------------------|------------------------|
|
|------|------------------|------------------------|
|
||||||
| 目的 | 默认可**升级**为根目录 `test_*.py` 的 **业务单元 / service 契约 / golden** 范式 | **外联联调**范式:网络、浏览器、仿真或真实系统 |
|
| 目的 | 默认可**升级**为根目录 `test_*.py` 的 **业务单元 / service 契约 / golden** 范式 | **外联联调**范式:网络、浏览器、仿真或真实系统 |
|
||||||
| 默认执行 | `.sample` 不执行;复制到根目录并改名后才进入默认套件 | **永远不**作为默认 unittest 的一部分;复制后仍需显式环境变量与手动/CI 任务触发 |
|
| 默认执行 | `.sample` 不执行;复制到根目录并改名后才进入默认套件 | **永远不**作为默认 unittest 的一部分;复制后仍需显式环境变量与手动/CI 任务触发 |
|
||||||
| 典型内容 | `FakeAdapter`、fixture、隔离 DB | `localhost` 仿真、真实 API(授权)、真实 RPA(最高风险) |
|
| 典型内容 | `FakeAdapter`、fixture、隔离 DB | `localhost` 仿真、真实 API、真实 RPA(最高风险) |
|
||||||
|
|
||||||
**原则**:只要会访问**网络**、**浏览器**、**真实或仿真外部系统**,应优先放在 `integration`(并保持 `.sample` 直至团队同意启用),而不是写进默认 `tests/test_*.py`。
|
**原则**:只要会访问**网络**、**浏览器**、**真实或仿真外部系统**,应优先放在 `integration`(并保持 `.sample` 直至团队同意启用),而不是写进默认 `tests/test_*.py`。
|
||||||
|
|
||||||
@@ -21,20 +21,15 @@
|
|||||||
|------|------------------------------|----------|
|
|------|------------------------------|----------|
|
||||||
| 本地 / CI 对接**仿真 HTTP** | ``simulator_api`` | 仿真服务如 ``http://localhost:5180`` |
|
| 本地 / CI 对接**仿真 HTTP** | ``simulator_api`` | 仿真服务如 ``http://localhost:5180`` |
|
||||||
| **仿真页面** / 录播 RPA | ``simulator_rpa`` | 本地页面或沙箱浏览器 profile |
|
| **仿真页面** / 录播 RPA | ``simulator_rpa`` | 本地页面或沙箱浏览器 profile |
|
||||||
| **真实 API**(只读优先) | ``real_api`` | ``ALLOW_REAL_API=1``;写操作另需 ``ALLOW_WRITE_ACTIONS=1`` |
|
| **真实 API** | ``real_api`` | **仅手动触发** |
|
||||||
| **真实 RPA**(最高风险) | ``real_rpa`` | ``ALLOW_REAL_RPA=1``;**仅手动触发** |
|
| **真实 RPA**(最高风险) | ``real_rpa`` | **仅手动触发** |
|
||||||
|
|
||||||
## 环境变量(与 ``adapter_test_utils`` 一致)
|
## 环境变量(与 ``adapter_test_utils`` 一致)
|
||||||
|
|
||||||
- **目标档位**(二选一键名,后者为历史拼写):``OPENCLAW_TEST_TARGET`` 或 ``OPENCLOW_TEST_TARGET``
|
- **目标档位**:``OPENCLAW_TEST_TARGET``
|
||||||
取值:``unit`` | ``mock`` | ``simulator_api`` | ``simulator_rpa`` | ``real_api`` | ``real_rpa``
|
取值:``unit`` | ``mock`` | ``simulator_api`` | ``simulator_rpa`` | ``real_api`` | ``real_rpa``
|
||||||
默认未设置等价于 ``unit``。
|
默认未设置等价于 ``unit``。
|
||||||
|
|
||||||
- **授权开关**(显式 ``1`` 才启用):
|
|
||||||
- ``ALLOW_REAL_API=1``
|
|
||||||
- ``ALLOW_REAL_RPA=1``
|
|
||||||
- ``ALLOW_WRITE_ACTIONS=1``(对写操作 / 提交表单类步骤)
|
|
||||||
|
|
||||||
## 安全约束
|
## 安全约束
|
||||||
|
|
||||||
- **禁止**在样例或复制后的测试代码中硬编码真实账号、密码、token、cookie、生产 URL。
|
- **禁止**在样例或复制后的测试代码中硬编码真实账号、密码、token、cookie、生产 URL。
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# 样例:复制为 test_real_api_contract.py;需 OPENCLAW_TEST_TARGET=real_api 且 ALLOW_REAL_API=1。
|
# 样例:复制为 test_real_api_contract.py;需 OPENCLAW_TEST_TARGET=real_api。
|
||||||
"""真实 HTTP API(只读优先;默认跳过)。"""
|
"""真实 HTTP API(只读优先;默认跳过)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ class TestRealApiContractSample(unittest.TestCase):
|
|||||||
raise unittest.SkipTest(reason)
|
raise unittest.SkipTest(reason)
|
||||||
# 写操作示例(复制后使用):
|
# 写操作示例(复制后使用):
|
||||||
# skip_w, _ = should_skip_profile("real_api", write=True)
|
# skip_w, _ = should_skip_profile("real_api", write=True)
|
||||||
# 需要 ALLOW_WRITE_ACTIONS=1
|
|
||||||
# 禁止在此文件写入真实 token;从 vault / 环境注入读取 endpoint。
|
# 禁止在此文件写入真实 token;从 vault / 环境注入读取 endpoint。
|
||||||
self.assertFalse(skip)
|
self.assertFalse(skip)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# 样例:复制为 test_real_rpa_contract.py;需 OPENCLAW_TEST_TARGET=real_rpa 且 ALLOW_REAL_RPA=1。
|
# 样例:复制为 test_real_rpa_contract.py;需 OPENCLAW_TEST_TARGET=real_rpa。
|
||||||
"""
|
"""
|
||||||
真实 RPA / 桌面自动化(**最高风险**,仅手动触发)。
|
真实 RPA / 桌面自动化(**最高风险**,仅手动触发)。
|
||||||
|
|
||||||
默认只读思路示例:打开沙箱门户、读取标题、**不提交表单**。
|
默认只读思路示例:打开沙箱门户、读取标题、**不提交表单**。
|
||||||
任何写操作必须 ``should_skip_profile('real_rpa', write=True)`` 且 ``ALLOW_WRITE_ACTIONS=1``。
|
写操作测试同样须显式设 ``OPENCLAW_TEST_TARGET=real_rpa`` 后手动触发。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from jiangchang_skill_core import config
|
||||||
|
|
||||||
from adapter_test_utils import (
|
from adapter_test_utils import (
|
||||||
ALLOWED_TEST_TARGETS,
|
ALLOWED_TEST_TARGETS,
|
||||||
FakeAdapter,
|
FakeAdapter,
|
||||||
@@ -24,34 +26,22 @@ def _restore_env(saved: dict[str, str | None]) -> None:
|
|||||||
os.environ.pop(k, None)
|
os.environ.pop(k, None)
|
||||||
else:
|
else:
|
||||||
os.environ[k] = v
|
os.environ[k] = v
|
||||||
|
config.reset_cache()
|
||||||
|
|
||||||
|
|
||||||
_ENV_KEYS = (
|
_ENV_KEYS = ("OPENCLAW_TEST_TARGET",)
|
||||||
"OPENCLAW_TEST_TARGET",
|
|
||||||
"OPENCLOW_TEST_TARGET",
|
|
||||||
"ALLOW_REAL_API",
|
|
||||||
"ALLOW_REAL_RPA",
|
|
||||||
"ALLOW_WRITE_ACTIONS",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestAdapterProfilePolicy(unittest.TestCase):
|
class TestAdapterProfilePolicy(unittest.TestCase):
|
||||||
def test_get_test_target_reads_opencylow_typo_alias(self) -> None:
|
def tearDown(self) -> None:
|
||||||
"""兼容环境变量历史拼写 OPENCLOW_TEST_TARGET。"""
|
config.reset_cache()
|
||||||
saved = _save_env(_ENV_KEYS)
|
|
||||||
try:
|
|
||||||
os.environ.pop("OPENCLAW_TEST_TARGET", None)
|
|
||||||
os.environ.pop("OPENCLOW_TEST_TARGET", None)
|
|
||||||
os.environ["OPENCLOW_TEST_TARGET"] = "mock"
|
|
||||||
self.assertEqual(get_test_target(), "mock")
|
|
||||||
finally:
|
|
||||||
_restore_env(saved)
|
|
||||||
|
|
||||||
def test_get_test_target_defaults_to_unit(self) -> None:
|
def test_get_test_target_defaults_to_unit(self) -> None:
|
||||||
saved = _save_env(_ENV_KEYS)
|
saved = _save_env(_ENV_KEYS)
|
||||||
try:
|
try:
|
||||||
for k in _ENV_KEYS:
|
for k in _ENV_KEYS:
|
||||||
os.environ.pop(k, None)
|
os.environ.pop(k, None)
|
||||||
|
config.reset_cache()
|
||||||
self.assertEqual(get_test_target(), "unit")
|
self.assertEqual(get_test_target(), "unit")
|
||||||
finally:
|
finally:
|
||||||
_restore_env(saved)
|
_restore_env(saved)
|
||||||
@@ -62,6 +52,7 @@ class TestAdapterProfilePolicy(unittest.TestCase):
|
|||||||
for k in _ENV_KEYS:
|
for k in _ENV_KEYS:
|
||||||
os.environ.pop(k, None)
|
os.environ.pop(k, None)
|
||||||
os.environ["OPENCLAW_TEST_TARGET"] = "not_a_valid_target"
|
os.environ["OPENCLAW_TEST_TARGET"] = "not_a_valid_target"
|
||||||
|
config.reset_cache()
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
get_test_target()
|
get_test_target()
|
||||||
finally:
|
finally:
|
||||||
@@ -74,6 +65,7 @@ class TestAdapterProfilePolicy(unittest.TestCase):
|
|||||||
saved = _save_env(_ENV_KEYS)
|
saved = _save_env(_ENV_KEYS)
|
||||||
try:
|
try:
|
||||||
os.environ["OPENCLAW_TEST_TARGET"] = "simulator_api"
|
os.environ["OPENCLAW_TEST_TARGET"] = "simulator_api"
|
||||||
|
config.reset_cache()
|
||||||
skip, _ = should_skip_profile("simulator_api")
|
skip, _ = should_skip_profile("simulator_api")
|
||||||
self.assertFalse(skip)
|
self.assertFalse(skip)
|
||||||
finally:
|
finally:
|
||||||
@@ -84,6 +76,7 @@ class TestAdapterProfilePolicy(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
for k in _ENV_KEYS:
|
for k in _ENV_KEYS:
|
||||||
os.environ.pop(k, None)
|
os.environ.pop(k, None)
|
||||||
|
config.reset_cache()
|
||||||
self.assertEqual(get_test_target(), "unit")
|
self.assertEqual(get_test_target(), "unit")
|
||||||
for prof in ("simulator_api", "simulator_rpa", "real_api", "real_rpa"):
|
for prof in ("simulator_api", "simulator_rpa", "real_api", "real_rpa"):
|
||||||
skip, reason = should_skip_profile(prof)
|
skip, reason = should_skip_profile(prof)
|
||||||
@@ -93,46 +86,23 @@ class TestAdapterProfilePolicy(unittest.TestCase):
|
|||||||
finally:
|
finally:
|
||||||
_restore_env(saved)
|
_restore_env(saved)
|
||||||
|
|
||||||
def test_real_api_requires_allow_flag(self) -> None:
|
def test_real_api_runs_when_target_matches(self) -> None:
|
||||||
saved = _save_env(_ENV_KEYS)
|
saved = _save_env(_ENV_KEYS)
|
||||||
try:
|
try:
|
||||||
os.environ["OPENCLAW_TEST_TARGET"] = "real_api"
|
os.environ["OPENCLAW_TEST_TARGET"] = "real_api"
|
||||||
os.environ.pop("ALLOW_REAL_API", None)
|
config.reset_cache()
|
||||||
skip, reason = should_skip_profile("real_api")
|
skip, _ = should_skip_profile("real_api", write=True)
|
||||||
self.assertTrue(skip)
|
self.assertFalse(skip)
|
||||||
self.assertIn("ALLOW_REAL_API", reason)
|
|
||||||
os.environ["ALLOW_REAL_API"] = "1"
|
|
||||||
skip2, _ = should_skip_profile("real_api", write=False)
|
|
||||||
self.assertFalse(skip2)
|
|
||||||
finally:
|
finally:
|
||||||
_restore_env(saved)
|
_restore_env(saved)
|
||||||
|
|
||||||
def test_real_api_write_requires_allow_write_actions(self) -> None:
|
def test_real_rpa_runs_when_target_matches(self) -> None:
|
||||||
saved = _save_env(_ENV_KEYS)
|
|
||||||
try:
|
|
||||||
os.environ["OPENCLAW_TEST_TARGET"] = "real_api"
|
|
||||||
os.environ["ALLOW_REAL_API"] = "1"
|
|
||||||
os.environ.pop("ALLOW_WRITE_ACTIONS", None)
|
|
||||||
skip, reason = should_skip_profile("real_api", write=True)
|
|
||||||
self.assertTrue(skip)
|
|
||||||
self.assertIn("ALLOW_WRITE_ACTIONS", reason)
|
|
||||||
os.environ["ALLOW_WRITE_ACTIONS"] = "1"
|
|
||||||
skip2, _ = should_skip_profile("real_api", write=True)
|
|
||||||
self.assertFalse(skip2)
|
|
||||||
finally:
|
|
||||||
_restore_env(saved)
|
|
||||||
|
|
||||||
def test_real_rpa_requires_allow_rpa_flag(self) -> None:
|
|
||||||
saved = _save_env(_ENV_KEYS)
|
saved = _save_env(_ENV_KEYS)
|
||||||
try:
|
try:
|
||||||
os.environ["OPENCLAW_TEST_TARGET"] = "real_rpa"
|
os.environ["OPENCLAW_TEST_TARGET"] = "real_rpa"
|
||||||
os.environ.pop("ALLOW_REAL_RPA", None)
|
config.reset_cache()
|
||||||
skip, reason = should_skip_profile("real_rpa")
|
skip, _ = should_skip_profile("real_rpa", write=True)
|
||||||
self.assertTrue(skip)
|
self.assertFalse(skip)
|
||||||
self.assertIn("ALLOW_REAL_RPA", reason)
|
|
||||||
os.environ["ALLOW_REAL_RPA"] = "1"
|
|
||||||
skip2, _ = should_skip_profile("real_rpa", write=False)
|
|
||||||
self.assertFalse(skip2)
|
|
||||||
finally:
|
finally:
|
||||||
_restore_env(saved)
|
_restore_env(saved)
|
||||||
|
|
||||||
@@ -146,10 +116,10 @@ class TestAdapterProfilePolicy(unittest.TestCase):
|
|||||||
FakeAdapter("unauthorized").call({})
|
FakeAdapter("unauthorized").call({})
|
||||||
|
|
||||||
def test_assert_no_real_network_env(self) -> None:
|
def test_assert_no_real_network_env(self) -> None:
|
||||||
saved = _save_env(("ALLOW_REAL_API", "ALLOW_REAL_RPA"))
|
saved = _save_env(_ENV_KEYS)
|
||||||
try:
|
try:
|
||||||
os.environ.pop("ALLOW_REAL_API", None)
|
os.environ.pop("OPENCLAW_TEST_TARGET", None)
|
||||||
os.environ.pop("ALLOW_REAL_RPA", None)
|
config.reset_cache()
|
||||||
assert_no_real_network_env()
|
assert_no_real_network_env()
|
||||||
finally:
|
finally:
|
||||||
_restore_env(saved)
|
_restore_env(saved)
|
||||||
|
|||||||
@@ -165,19 +165,19 @@ class TestDocsStandards(unittest.TestCase):
|
|||||||
self.assertIn("RpaVideoSession", text)
|
self.assertIn("RpaVideoSession", text)
|
||||||
self.assertIn("1.0.14", text)
|
self.assertIn("1.0.14", text)
|
||||||
|
|
||||||
def test_adapter_md_covers_four_tiers_and_allow_flags(self) -> None:
|
def test_adapter_md_covers_four_tiers_and_config_get(self) -> None:
|
||||||
text = self._read("development/ADAPTER.md")
|
text = self._read("development/ADAPTER.md")
|
||||||
for marker in (
|
for marker in (
|
||||||
"mock",
|
"mock",
|
||||||
"simulator_rpa",
|
"simulator_rpa",
|
||||||
"real_api",
|
"real_api",
|
||||||
"real_rpa",
|
"real_rpa",
|
||||||
"ALLOW_REAL_API",
|
"config.get",
|
||||||
"ALLOW_REAL_RPA",
|
|
||||||
"ALLOW_WRITE_ACTIONS",
|
|
||||||
"sibling_bridge",
|
"sibling_bridge",
|
||||||
):
|
):
|
||||||
self.assertIn(marker, text, msg=f"ADAPTER.md missing {marker!r}")
|
self.assertIn(marker, text, msg=f"ADAPTER.md missing {marker!r}")
|
||||||
|
self.assertNotIn("ALLOW_REAL_API", text)
|
||||||
|
self.assertNotIn("ALLOW_WRITE_ACTIONS", text)
|
||||||
|
|
||||||
def test_cli_md_mentions_shared_python_runtime(self) -> None:
|
def test_cli_md_mentions_shared_python_runtime(self) -> None:
|
||||||
text = self._read("references/CLI.md")
|
text = self._read("references/CLI.md")
|
||||||
|
|||||||
Reference in New Issue
Block a user