chore: auto release commit (2026-07-11 15:57:40)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# ── 运行模式 / adapter 档位(见 references/ADAPTER.md)──
|
# ── 运行模式 / adapter 档位(见 development/ADAPTER.md)──
|
||||||
OPENCLAW_TEST_TARGET=mock # mock | simulator_rpa | real_api | real_rpa
|
OPENCLAW_TEST_TARGET=mock # mock | simulator_rpa | real_api | real_rpa
|
||||||
|
|
||||||
# ── 目标系统 ──
|
# ── 目标系统 ──
|
||||||
@@ -12,7 +12,7 @@ OPENCLAW_BROWSER_HEADLESS=0 # 0=有头(默认) 1=无头(CI)
|
|||||||
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
|
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
|
||||||
|
|
||||||
# ── 存证 ──
|
# ── 存证 ──
|
||||||
OPENCLAW_RECORD_VIDEO=1
|
OPENCLAW_RECORD_VIDEO=0 # 0=默认不录屏;1=启用录屏+字幕+旁白+背景音+MP4
|
||||||
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
|
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
|
||||||
|
|
||||||
# ── 节流 / 超时 ──
|
# ── 节流 / 超时 ──
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ OPENCLAW_BROWSER_HEADLESS=0 # 0=有头(默认) 1=无头(CI)
|
|||||||
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
|
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
|
||||||
|
|
||||||
# ── 录屏与失败存证 ──
|
# ── 录屏与失败存证 ──
|
||||||
OPENCLAW_RECORD_VIDEO=1 # 1=录屏+字幕+旁白+背景音+MP4(RPA 默认开,见 RPA.md)
|
OPENCLAW_RECORD_VIDEO=0 # 0=默认不录屏;1=启用录屏+字幕+旁白+背景音+MP4(见 RPA.md)
|
||||||
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
|
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
|
||||||
|
|
||||||
# ── 节流 / 超时 ──
|
# ── 节流 / 超时 ──
|
||||||
@@ -42,6 +42,13 @@ HUMAN_WAIT_TIMEOUT=180 # 滑块/验证码/2FA 等人工超时(
|
|||||||
|
|
||||||
**业务专属配置必须带技能前缀**(如 `DEMO_XXX`、`MY_SKILL_XXX`),**不要污染全局命名空间**(禁止 `SCRAPE_1688_*`、`RECEIVE_ORDER_*` 等跨技能前缀写入模板)。
|
**业务专属配置必须带技能前缀**(如 `DEMO_XXX`、`MY_SKILL_XXX`),**不要污染全局命名空间**(禁止 `SCRAPE_1688_*`、`RECEIVE_ORDER_*` 等跨技能前缀写入模板)。
|
||||||
|
|
||||||
|
### 录屏开关(`OPENCLAW_RECORD_VIDEO`)
|
||||||
|
|
||||||
|
- **模板默认 `OPENCLAW_RECORD_VIDEO=0`**,降低本地开发 / 调试成本(不启 ffmpeg、不生成 MP4)。
|
||||||
|
- 需要**生产留证、合规记录、客户现场复现或排障**时,可在用户 `.env` 或进程环境变量中设为 `OPENCLAW_RECORD_VIDEO=1`。
|
||||||
|
- **默认关闭不代表可以删除录屏接入**:RPA / 长任务代码仍须保留 `RpaVideoSession`、`video.add_step` 与 video artifact 写入 `result_summary`(见 `RPA.md` §5.3)。
|
||||||
|
- **已落盘的用户 `.env` 不会被模板整体覆盖**;老用户保留原值,技能升级时仅通过 `merge_missing_env_keys` **追加** `.env.example` 中新增的 key。
|
||||||
|
|
||||||
## 🚫 红线:敏感信息不进 `.env`
|
## 🚫 红线:敏感信息不进 `.env`
|
||||||
|
|
||||||
`.env` 只放**非敏感运行参数**(模式、地址、开关、超时、账号标识)。密码、密钥、动态口令、token——
|
`.env` 只放**非敏感运行参数**(模式、地址、开关、超时、账号标识)。密码、密钥、动态口令、token——
|
||||||
|
|||||||
@@ -88,11 +88,13 @@ OpenClaw 技能常在客户电脑上异步执行,且大量依赖 RPA、浏览
|
|||||||
|
|
||||||
**来源**:`RpaVideoSession`、`rpa-artifacts/` 目录
|
**来源**:`RpaVideoSession`、`rpa-artifacts/` 目录
|
||||||
|
|
||||||
|
- video artifact 是**标准可启用能力**,**不保证默认每次生成 MP4**(模板默认 `OPENCLAW_RECORD_VIDEO=0`)。
|
||||||
|
- **`OPENCLAW_RECORD_VIDEO=0`**:不生成最终录屏,但 RPA / 长任务仍须保留 `RpaVideoSession`、`video.add_step` 与 Activity 进度同步。
|
||||||
|
- **`OPENCLAW_RECORD_VIDEO=1`**:`result_summary` 应包含 `video_path`、`raw_video`、`video_log`、`video_warnings` 等字段;CLI 可打印录屏路径与诊断。
|
||||||
- 用户可见的中文步骤(「打开登录页」「点击提交」)。
|
- 用户可见的中文步骤(「打开登录页」「点击提交」)。
|
||||||
- 录屏 MP4、`video_log`(ffmpeg 诊断)。
|
- 失败截图:`{数据目录}/rpa-artifacts/{batch_id}/...`(受 `OPENCLAW_ARTIFACTS_ON_FAILURE` 控制,默认开)
|
||||||
- 失败截图:`{数据目录}/rpa-artifacts/{batch_id}/...`
|
|
||||||
|
|
||||||
详见 [`RPA.md`](RPA.md) §失败存证与录屏标准。
|
详见 [`RPA.md`](RPA.md) §5 存证与录屏规范。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -254,7 +256,7 @@ def cmd_run(target=None, input_id=None):
|
|||||||
3. **`python scripts/main.py log-get <id>`** — 单条 JSON,含 `result_summary`(video 路径等)。
|
3. **`python scripts/main.py log-get <id>`** — 单条 JSON,含 `result_summary`(video 路径等)。
|
||||||
4. **统一日志文件** — `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(grep `trace_id`、`task_failed`)。
|
4. **统一日志文件** — `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(grep `trace_id`、`task_failed`)。
|
||||||
5. **Run Journal** — `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`(宿主 UI 进度来源)。
|
5. **Run Journal** — `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`(宿主 UI 进度来源)。
|
||||||
6. **RPA artifacts** — `rpa-artifacts/` 截图、录屏 MP4、`video_log`。
|
6. **RPA artifacts** — `rpa-artifacts/` 失败截图;若 `OPENCLAW_RECORD_VIDEO=1` 还有录屏 MP4、`video_log`。未开启录屏时可能只有截图或日志,需要完整录屏请在用户 `.env` 中设置 `OPENCLAW_RECORD_VIDEO=1`。
|
||||||
|
|
||||||
长时间无 stdout **不代表卡死**;先看 Run Journal / Activity,再看统一日志。
|
长时间无 stdout **不代表卡死**;先看 Run Journal / Activity,再看统一日志。
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
| POLICY-TESTING-002 | `tests/integration/` 下 Python 测试文件若存在,默认应为 `.sample`,不得进入默认套件 | development/TESTING.md §7 真实联调测试的安全约束、§8 新 skill 的最小测试清单 | hard | 检查 `tests/integration/*.py` 无 `.sample` 后缀 | `tests/test_development_policy_guard.py::TestPolicyTesting002` |
|
| POLICY-TESTING-002 | `tests/integration/` 下 Python 测试文件若存在,默认应为 `.sample`,不得进入默认套件 | development/TESTING.md §7 真实联调测试的安全约束、§8 新 skill 的最小测试清单 | hard | 检查 `tests/integration/*.py` 无 `.sample` 后缀 | `tests/test_development_policy_guard.py::TestPolicyTesting002` |
|
||||||
| POLICY-RPA-001 | 不得 import/use account-manager 内部 `rpa_helpers` / `inject_account_manager_scripts_path` / `get_account_credential` | development/ADAPTER.md §兄弟依赖;development/RPA.md §1.7 | hard | 扫描 `scripts/**/*.py` 禁止模式 | `tests/test_development_policy_guard.py::TestPolicyRpa001`(另见 `tests/test_no_rpa_helpers_import.py`) |
|
| POLICY-RPA-001 | 不得 import/use account-manager 内部 `rpa_helpers` / `inject_account_manager_scripts_path` / `get_account_credential` | development/ADAPTER.md §兄弟依赖;development/RPA.md §1.7 | hard | 扫描 `scripts/**/*.py` 禁止模式 | `tests/test_development_policy_guard.py::TestPolicyRpa001`(另见 `tests/test_no_rpa_helpers_import.py`) |
|
||||||
| POLICY-RPA-002 | RPA 交付代码不得直接执行 ffmpeg 拼 MP4;应使用 `RpaVideoSession`(允许 `ffmpeg_path` 等诊断字段) | development/RPA.md §5.3 录屏成片标准 | hard | 扫描 `scripts/**/*.py` 禁止 `subprocess` / `os.system` / `os.popen` 直接调用 ffmpeg(`task_run_support.py` 保留豁免) | `tests/test_development_policy_guard.py::TestPolicyRpa002` |
|
| POLICY-RPA-002 | RPA 交付代码不得直接执行 ffmpeg 拼 MP4;应使用 `RpaVideoSession`(允许 `ffmpeg_path` 等诊断字段) | development/RPA.md §5.3 录屏成片标准 | hard | 扫描 `scripts/**/*.py` 禁止 `subprocess` / `os.system` / `os.popen` 直接调用 ffmpeg(`task_run_support.py` 保留豁免) | `tests/test_development_policy_guard.py::TestPolicyRpa002` |
|
||||||
|
| POLICY-RPA-003 | 即使默认 `OPENCLAW_RECORD_VIDEO=0`,RPA / 长任务模板也必须保留录屏能力:`RpaVideoSession`、`video.add_step`、video artifact merge 到 `result_summary` | development/RPA.md §5.3;development/LOGGING.md;development/CONFIG.md | hard | 扫描 `scripts/service/*.py`(service 层整体须含上述接入) | `tests/test_development_policy_guard.py::TestPolicyRpa003` |
|
||||||
| POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束;development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` |
|
| POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束;development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` |
|
||||||
| POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM,无 `U+FEFF` | development/DEVELOPMENT.md §3.4;development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` |
|
| POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM,无 `U+FEFF` | development/DEVELOPMENT.md §3.4;development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` |
|
||||||
| POLICY-DOCS-001 | 本矩阵存在且包含上述全部 `policy_id` | 本次规范化约定 | hard | 解析 `development/POLICY_MATRIX.md` | `tests/test_development_policy_guard.py::TestPolicyDocs001` |
|
| POLICY-DOCS-001 | 本矩阵存在且包含上述全部 `policy_id` | 本次规范化约定 | hard | 解析 `development/POLICY_MATRIX.md` | `tests/test_development_policy_guard.py::TestPolicyDocs001` |
|
||||||
|
|||||||
@@ -283,13 +283,16 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层
|
|||||||
|
|
||||||
### 5.3 录屏成片标准
|
### 5.3 录屏成片标准
|
||||||
|
|
||||||
- RPA skill 默认 `OPENCLAW_RECORD_VIDEO=1`。
|
- **skill-template 默认 `OPENCLAW_RECORD_VIDEO=0`**(开发调试更快,不启 ffmpeg)。
|
||||||
|
- **`OPENCLAW_RECORD_VIDEO=1` 时启用录屏**:字幕、旁白、背景音、MP4 成片由 platform-kit 统一处理。
|
||||||
- 使用 platform-kit 的 **`RpaVideoSession`**;**skill 不要自行合成视频**(不要自己调 ffmpeg 拼 MP4)。
|
- 使用 platform-kit 的 **`RpaVideoSession`**;**skill 不要自行合成视频**(不要自己调 ffmpeg 拼 MP4)。
|
||||||
- `OPENCLAW_RECORD_VIDEO=0` 时 session 无副作用(不启 ffmpeg、不写字幕文件)。
|
- **默认关闭 ≠ 可以删除录屏接入**:RPA / 长任务 **service 层**(`scripts/service/*.py`)**必须保留** `RpaVideoSession`、`video.add_step`、video artifact merge 到 `result_summary`(见 `POLICY-RPA-003`)。
|
||||||
|
- `OPENCLAW_RECORD_VIDEO=0` 时 session **无副作用**(不启 ffmpeg、不写字幕文件、不生成最终 MP4),但 **`RpaVideoSession.add_step` 仍可同步 Activity 进度**。
|
||||||
|
- 生产留证 / 合规 / 客户现场复现时,由用户在 `.env` 或进程环境变量中开启 `OPENCLAW_RECORD_VIDEO=1`。
|
||||||
- **ffmpeg 是唯一录屏器**(Windows:`gdigrab` + `desktop`)。
|
- **ffmpeg 是唯一录屏器**(Windows:`gdigrab` + `desktop`)。
|
||||||
- **最终视频**:`{skill_data_dir}/videos/{skill_slug}_{yyyyMMdd_HHmmss}_{batch_id}.mp4`
|
- **最终视频**(`OPENCLAW_RECORD_VIDEO=1` 时):`{skill_data_dir}/videos/{skill_slug}_{yyyyMMdd_HHmmss}_{batch_id}.mp4`
|
||||||
- **中间产物**:`rpa-artifacts/{batch_id}/capture.mp4`、`subtitles/`、`logs/` 等。
|
- **中间产物**:`rpa-artifacts/{batch_id}/capture.mp4`、`subtitles/`、`logs/` 等。
|
||||||
- 任务完成后 CLI / `result_summary` 应包含:`video_path`、`raw_video`、`video_log`、`video_warnings`、`music_path`、`voiceover_path`、`audio_warnings`(见 `scripts/service/task_run_support.py`)。
|
- 任务完成后 CLI / `result_summary` 应包含 video 字段(开启录屏时有路径;关闭时仍保留 `video` 结构与诊断占位):`video_path`、`raw_video`、`video_log`、`video_warnings`、`music_path`、`voiceover_path`、`audio_warnings`(见 `scripts/service/task_run_support.py`)。
|
||||||
|
|
||||||
模板最小示范见 `scripts/service/task_service.py` 的 `_run_template_demo()`。
|
模板最小示范见 `scripts/service/task_service.py` 的 `_run_template_demo()`。
|
||||||
|
|
||||||
|
|||||||
@@ -236,10 +236,11 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
|
|||||||
|
|
||||||
## 12. RPA / video 测试标准
|
## 12. RPA / video 测试标准
|
||||||
|
|
||||||
- `RpaVideoSession` 调用**不跑真实 ffmpeg**:单测中使用 `unittest.mock` patch session 或设 `OPENCLAW_RECORD_VIDEO=0`。
|
- 模板 `.env.example` 默认 **`OPENCLAW_RECORD_VIDEO=0`**;默认必跑测试**不应启动真实 ffmpeg**(patch session 或显式设 `OPENCLAW_RECORD_VIDEO=0`)。
|
||||||
|
- **但测试必须守护录屏接入能力**(`POLICY-RPA-003` 扫描 `scripts/service/*.py`):`RpaVideoSession` 被构造、`video.add_step` 被调用、video artifact 结构 merge 进 `result_summary`(`video` / `video_path` 等字段),与默认是否录屏无关。
|
||||||
- 断言 `title` / `closing_title` 是**中文**业务文案。
|
- 断言 `title` / `closing_title` 是**中文**业务文案。
|
||||||
- 断言 video artifact 会进入 `result_summary`(`video_path`、`raw_video`、`video_log` 等)。
|
|
||||||
- 断言 step 文案贴近用户动作,不是技术日志(如「准备执行示例任务」而非「enter cmd_run」)。
|
- 断言 step 文案贴近用户动作,不是技术日志(如「准备执行示例任务」而非「enter cmd_run」)。
|
||||||
|
- 若需测试**真实录屏**(ffmpeg 成片),必须显式设 `OPENCLAW_RECORD_VIDEO=1`,并放在 `tests/integration/` 或手动触发场景,**不得**进入默认 unittest 套件。
|
||||||
|
|
||||||
参考 `tests/test_video_service.py`。
|
参考 `tests/test_video_service.py`。
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ python {baseDir}/scripts/main.py version
|
|||||||
2. `python {baseDir}/scripts/main.py log-get <log_id>`
|
2. `python {baseDir}/scripts/main.py log-get <log_id>`
|
||||||
3. 统一日志文件 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(含 `trace_id`、`task_failed` 等)
|
3. 统一日志文件 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(含 `trace_id`、`task_failed` 等)
|
||||||
4. Run Journal `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`
|
4. Run Journal `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`
|
||||||
5. RPA artifacts / `video_log` / 失败截图(见 `development/RPA.md`)
|
5. RPA artifacts / `video_log` / 失败截图(见 `development/RPA.md`);未开启录屏时可能只有截图或日志
|
||||||
- **任务完成后**若有 video artifact,CLI 应打印录屏路径、录屏日志、视频/音频诊断(见 `task_run_support._print_video_summary`)。
|
- **录屏默认关闭**:模板 `.env.example` 为 `OPENCLAW_RECORD_VIDEO=0`;若未开启录屏,任务**可能不会**打印录屏路径。
|
||||||
|
- 需要录屏时在用户 `.env` 或进程环境变量中设置 **`OPENCLAW_RECORD_VIDEO=1`**;开启后任务完成时 CLI 应打印录屏路径、录屏日志、视频/音频诊断(见 `task_run_support._print_video_summary`),且 `result_summary` 含 video 字段。
|
||||||
|
|
||||||
## config-path(配置路径)
|
## config-path(配置路径)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ POLICY_IDS = (
|
|||||||
"POLICY-TESTING-002",
|
"POLICY-TESTING-002",
|
||||||
"POLICY-RPA-001",
|
"POLICY-RPA-001",
|
||||||
"POLICY-RPA-002",
|
"POLICY-RPA-002",
|
||||||
|
"POLICY-RPA-003",
|
||||||
"POLICY-PACKAGING-001",
|
"POLICY-PACKAGING-001",
|
||||||
"POLICY-PACKAGING-002",
|
"POLICY-PACKAGING-002",
|
||||||
"POLICY-DOCS-001",
|
"POLICY-DOCS-001",
|
||||||
@@ -109,6 +110,10 @@ RPA_002_FFMPEG_PATTERNS = (
|
|||||||
|
|
||||||
RPA_002_EXEMPT_REL = "scripts/service/task_run_support.py"
|
RPA_002_EXEMPT_REL = "scripts/service/task_run_support.py"
|
||||||
|
|
||||||
|
RPA_003_SOURCE = (
|
||||||
|
"development/RPA.md §5.3; development/TESTING.md §12; development/POLICY_MATRIX.md"
|
||||||
|
)
|
||||||
|
|
||||||
LOGGING_001_SOURCE = "development/LOGGING.md; development/RUNTIME.md"
|
LOGGING_001_SOURCE = "development/LOGGING.md; development/RUNTIME.md"
|
||||||
LOGGING_002_SOURCE = "development/LOGGING.md; development/DEVELOPMENT.md"
|
LOGGING_002_SOURCE = "development/LOGGING.md; development/DEVELOPMENT.md"
|
||||||
LOGGING_003_SOURCE = "development/LOGGING.md"
|
LOGGING_003_SOURCE = "development/LOGGING.md"
|
||||||
@@ -470,6 +475,30 @@ class TestPolicyRpa002(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPolicyRpa003(unittest.TestCase):
|
||||||
|
def test_service_layer_retains_video_session_integration(self) -> None:
|
||||||
|
skill_root = get_skill_root()
|
||||||
|
service_files = _walk_files(skill_root, "scripts/service", suffix=".py")
|
||||||
|
self.assertTrue(service_files, msg="scripts/service/*.py not found")
|
||||||
|
combined = "\n".join(_read_text(path) for path in service_files)
|
||||||
|
missing: list[str] = []
|
||||||
|
if "RpaVideoSession" not in combined:
|
||||||
|
missing.append("RpaVideoSession")
|
||||||
|
if "video.add_step" not in combined and ".add_step(" not in combined:
|
||||||
|
missing.append("video.add_step or .add_step(")
|
||||||
|
if "merge_video_into_result_summary" not in combined:
|
||||||
|
missing.append("merge_video_into_result_summary")
|
||||||
|
self.assertEqual(
|
||||||
|
missing,
|
||||||
|
[],
|
||||||
|
msg=_policy_msg(
|
||||||
|
"POLICY-RPA-003",
|
||||||
|
RPA_003_SOURCE,
|
||||||
|
"scripts/service/*.py missing: " + ", ".join(missing),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _scan_sensitive_logging_assignments(skill_root: str) -> list[str]:
|
def _scan_sensitive_logging_assignments(skill_root: str) -> list[str]:
|
||||||
offenders: list[str] = []
|
offenders: list[str] = []
|
||||||
for path in _walk_files(skill_root, "scripts", suffix=".py"):
|
for path in _walk_files(skill_root, "scripts", suffix=".py"):
|
||||||
|
|||||||
@@ -15,6 +15,46 @@ from _support import IsolatedDataRoot, get_skill_root
|
|||||||
from jiangchang_skill_core import config
|
from jiangchang_skill_core import config
|
||||||
|
|
||||||
|
|
||||||
|
def _active_env_value(text: str, key: str) -> str | None:
|
||||||
|
"""Return the active (non-comment) KEY=value line from .env-style text."""
|
||||||
|
prefix = f"{key}="
|
||||||
|
for line in text.splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
active = stripped.split("#", 1)[0].strip()
|
||||||
|
if active.startswith(prefix):
|
||||||
|
return active
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _combined_service_source(skill_root: str | None = None) -> str:
|
||||||
|
"""Merge scripts/service/*.py source (excludes __pycache__)."""
|
||||||
|
root = skill_root or get_skill_root()
|
||||||
|
service_dir = os.path.join(root, "scripts", "service")
|
||||||
|
parts: list[str] = []
|
||||||
|
for dirpath, dirnames, filenames in os.walk(service_dir):
|
||||||
|
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
|
||||||
|
for name in sorted(filenames):
|
||||||
|
if not name.endswith(".py"):
|
||||||
|
continue
|
||||||
|
path = os.path.join(dirpath, name)
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
parts.append(f.read())
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _missing_video_integration_tokens(service_text: str) -> list[str]:
|
||||||
|
missing: list[str] = []
|
||||||
|
if "RpaVideoSession" not in service_text:
|
||||||
|
missing.append("RpaVideoSession")
|
||||||
|
if "video.add_step" not in service_text and ".add_step(" not in service_text:
|
||||||
|
missing.append("video.add_step or .add_step(")
|
||||||
|
if "merge_video_into_result_summary" not in service_text:
|
||||||
|
missing.append("merge_video_into_result_summary")
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
class TestEnvExampleVideoDefaults(unittest.TestCase):
|
class TestEnvExampleVideoDefaults(unittest.TestCase):
|
||||||
def test_env_example_contains_required_keys(self) -> None:
|
def test_env_example_contains_required_keys(self) -> None:
|
||||||
path = os.path.join(get_skill_root(), ".env.example")
|
path = os.path.join(get_skill_root(), ".env.example")
|
||||||
@@ -22,13 +62,45 @@ class TestEnvExampleVideoDefaults(unittest.TestCase):
|
|||||||
text = f.read()
|
text = f.read()
|
||||||
for key in (
|
for key in (
|
||||||
"OPENCLAW_" + "TEST_TARGET=mock",
|
"OPENCLAW_" + "TEST_TARGET=mock",
|
||||||
"OPENCLAW_RECORD_VIDEO=1",
|
"OPENCLAW_RECORD_VIDEO=0",
|
||||||
"OPENCLAW_ARTIFACTS_ON_FAILURE=1",
|
"OPENCLAW_ARTIFACTS_ON_FAILURE=1",
|
||||||
"OPENCLAW_BROWSER_HEADLESS=0",
|
"OPENCLAW_BROWSER_HEADLESS=0",
|
||||||
"OPENCLAW_PLAYWRIGHT_STEALTH=1",
|
"OPENCLAW_PLAYWRIGHT_STEALTH=1",
|
||||||
):
|
):
|
||||||
self.assertIn(key, text, msg=f"missing {key!r} in .env.example")
|
self.assertIn(key, text, msg=f"missing {key!r} in .env.example")
|
||||||
|
|
||||||
|
def test_env_example_default_off_but_template_keeps_video_integration(self) -> None:
|
||||||
|
path = os.path.join(get_skill_root(), ".env.example")
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
self.assertEqual(
|
||||||
|
_active_env_value(text, "OPENCLAW_RECORD_VIDEO"),
|
||||||
|
"OPENCLAW_RECORD_VIDEO=0",
|
||||||
|
)
|
||||||
|
|
||||||
|
service_text = _combined_service_source()
|
||||||
|
missing = _missing_video_integration_tokens(service_text)
|
||||||
|
self.assertEqual(
|
||||||
|
missing,
|
||||||
|
[],
|
||||||
|
msg=(
|
||||||
|
"default OPENCLAW_RECORD_VIDEO=0 does not allow removing video integration; "
|
||||||
|
f"scripts/service/*.py missing: {', '.join(missing)}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_env_example_allows_record_video_one_in_comments_only(self) -> None:
|
||||||
|
sample = "\n".join(
|
||||||
|
[
|
||||||
|
"# example: OPENCLAW_RECORD_VIDEO=1 for production",
|
||||||
|
"OPENCLAW_RECORD_VIDEO=0",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
_active_env_value(sample, "OPENCLAW_RECORD_VIDEO"),
|
||||||
|
"OPENCLAW_RECORD_VIDEO=0",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestPrintVideoSummary(unittest.TestCase):
|
class TestPrintVideoSummary(unittest.TestCase):
|
||||||
def test_cli_prints_video_path_when_enabled(self) -> None:
|
def test_cli_prints_video_path_when_enabled(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user