From 144455fbe05ce962a6823140772e00c225a60592 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Fri, 17 Jul 2026 19:06:21 +0800 Subject: [PATCH] test(config): add POLICY-CONFIG-004 hard gate for user-facing .env.example comments (v1.0.48) --- CHANGELOG.md | 4 + SKILL.md | 2 +- development/CONFIG.md | 2 + development/POLICY_MATRIX.md | 1 + scripts/util/constants.py | 2 +- tests/test_development_policy_guard.py | 105 +++++++++++++++++++++++++ 6 files changed, 114 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c46c58a..5566056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.48 + +- 新增配置注释硬门禁 `POLICY-CONFIG-004`:`.env.example` 每个配置项必须有中文行头与行尾说明,禁止开发文档指向,测试不通过则无法发布 + ## 1.0.47 - 规范 `.env.example` 注释:面向「数据管理 → 配置管理」用户,行头说明「是什么」、行尾说明「怎么填」 diff --git a/SKILL.md b/SKILL.md index 335013f..13c80e9 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.47 +version: 1.0.48 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/development/CONFIG.md b/development/CONFIG.md index 7857b6f..cc7c862 100644 --- a/development/CONFIG.md +++ b/development/CONFIG.md @@ -33,6 +33,8 @@ 3. 业务专属 key 同样适用本规则,不得只留英文 key、不写中文说明。 4. 开发向细节(四档 adapter 实现、录屏管线、路径解析 API)写在 `development/ADAPTER.md`、`RPA.md`、`DATA_PATHS.md` 等文档;**禁止**写进 `.env.example` 注释。 +**机器门禁**:`POLICY-CONFIG-004`(`tests/test_development_policy_guard.py`)会在默认测试 / `release.ps1` 中校验上述「行头+行尾中文、禁止开发文档指向」;不通过不得发布。 + ### 禁止出现在注释中的内容 - `见 development/...`、`见 ADAPTER.md`、`见 RPA.md`、`见 DATA_PATHS.md` 等仓库文档指向 diff --git a/development/POLICY_MATRIX.md b/development/POLICY_MATRIX.md index fa17f60..84376e2 100644 --- a/development/POLICY_MATRIX.md +++ b/development/POLICY_MATRIX.md @@ -13,6 +13,7 @@ | POLICY-CONFIG-001 | 仓库根目录必须存在 `.env.example` | development/CONFIG.md §核心原则、§标准配置项 | hard | 文件存在性 | `tests/test_development_policy_guard.py::TestPolicyConfig001` | | POLICY-CONFIG-002 | `.gitignore` 必须忽略 `.env` 与 `*.env.local` | development/CONFIG.md §红线:敏感信息不进 .env | hard | 解析 `.gitignore` 行 | `tests/test_development_policy_guard.py::TestPolicyConfig002` | | POLICY-CONFIG-003 | 业务代码不得直接 `os.environ` / `os.getenv` / `os.putenv` / `environ.get`;须经 `jiangchang_skill_core.config.get*`(允许 `main.py`、测试、config_bootstrap/runtime 边界文件) | development/CONFIG.md §配置 bootstrap;development/ADAPTER.md §档位 dispatch | hard | 扫描 `scripts/**/*.py` 并排除边界文件 | `tests/test_development_policy_guard.py::TestPolicyConfig003` | +| POLICY-CONFIG-004 | `.env.example` 每个活跃 `KEY=` 必须有中文**行头**(是什么)与中文**行尾**(怎么填);注释禁止 `development/`、`*.md` 文档指向、`account-manager`、`adapter 档位` 等开发黑话 | development/CONFIG.md §.env.example 注释 = 配置管理用户文案 | hard | 解析 `.env.example` 活跃行与注释 | `tests/test_development_policy_guard.py::TestPolicyConfig004` | | POLICY-TESTING-001 | 默认根目录 `tests/test_*.py` 不得硬编码 `real_api` / `real_rpa` / `OPENCLAW_TEST_TARGET`(白名单仅 `test_adapter_profile_policy.py`,用于档位 helper 自测) | development/TESTING.md §默认必跑测试要做什么、§4 测试目标档位 | hard | 扫描 `tests/test_*.py` 非注释行;白名单外禁止上述文本 | `tests/test_development_policy_guard.py::TestPolicyTesting001` | | 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`) | diff --git a/scripts/util/constants.py b/scripts/util/constants.py index b15f87f..8058a28 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.47" +SKILL_VERSION = "1.0.48" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.0" diff --git a/tests/test_development_policy_guard.py b/tests/test_development_policy_guard.py index 601d312..c74e2da 100644 --- a/tests/test_development_policy_guard.py +++ b/tests/test_development_policy_guard.py @@ -29,6 +29,7 @@ POLICY_IDS = ( "POLICY-CONFIG-001", "POLICY-CONFIG-002", "POLICY-CONFIG-003", + "POLICY-CONFIG-004", "POLICY-TESTING-001", "POLICY-TESTING-002", "POLICY-RPA-001", @@ -86,6 +87,22 @@ CONFIG_ENV_FORBIDDEN_PATTERNS = ( re.compile(r"\benviron\.get\b"), ) +CONFIG_004_SOURCE = "development/CONFIG.md §.env.example 注释 = 配置管理用户文案" +CONFIG_004_ENV_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +CONFIG_004_CJK_RE = re.compile(r"[\u4e00-\u9fff]") +# 注释中禁止出现的开发向指向 / 黑话(大小写不敏感子串) +CONFIG_004_FORBIDDEN_COMMENT_SUBSTRINGS = ( + "development/", + "adapter.md", + "rpa.md", + "data_paths.md", + "config.md", + "account-manager", + "见 development", + "adapter 档位", + "adapter档位", +) + TESTING_REAL_TARGET_ALLOWED_ROOT_TESTS = frozenset({"test_adapter_profile_policy.py"}) TESTING_001_FORBIDDEN_SUBSTRINGS = ( @@ -149,6 +166,77 @@ def _policy_msg(policy_id: str, source: str, detail: str) -> str: return f"{policy_id} [{source}]: {detail}" +def _strip_env_quotes(value: str) -> str: + trimmed = value.strip() + if ( + (trimmed.startswith('"') and trimmed.endswith('"')) + or (trimmed.startswith("'") and trimmed.endswith("'")) + ): + return trimmed[1:-1] + return trimmed + + +def _iter_env_example_active_entries(path: str): + """Yield (lineno, key, value, header_comments, inline_comment) for active KEY= lines.""" + pending: list[str] = [] + with open(path, encoding="utf-8") as handle: + for lineno, line in enumerate(handle, 1): + stripped = line.strip() + if not stripped: + pending = [] + continue + if stripped.startswith("#"): + pending.append(stripped.lstrip("#").strip()) + continue + eq = stripped.find("=") + if eq <= 0: + pending = [] + continue + key = stripped[:eq].strip() + if not CONFIG_004_ENV_KEY_RE.match(key): + pending = [] + continue + rest = stripped[eq + 1 :] + inline: str | None = None + if not rest.startswith('"') and not rest.startswith("'"): + hash_at = rest.find(" #") + if hash_at >= 0: + inline = rest[hash_at + 2 :].strip() + rest = rest[:hash_at] + value = _strip_env_quotes(rest) + yield lineno, key, value, list(pending), inline + pending = [] + + +def _config_004_comment_offenders(path: str) -> list[str]: + offenders: list[str] = [] + for lineno, key, _value, headers, inline in _iter_env_example_active_entries(path): + if not headers: + offenders.append( + f".env.example:{lineno}: {key}= 缺少行头注释(须用中文说明「是什么」)" + ) + elif not any(CONFIG_004_CJK_RE.search(part) for part in headers): + offenders.append( + f".env.example:{lineno}: {key}= 行头注释须含中文(说明「是什么」)" + ) + if not inline: + offenders.append( + f".env.example:{lineno}: {key}= 缺少行尾注释(须用中文说明「怎么填」)" + ) + elif not CONFIG_004_CJK_RE.search(inline): + offenders.append( + f".env.example:{lineno}: {key}= 行尾注释须含中文(说明「怎么填」)" + ) + combined = "\n".join([*headers, inline or ""]).lower() + for token in CONFIG_004_FORBIDDEN_COMMENT_SUBSTRINGS: + if token.lower() in combined: + offenders.append( + f".env.example:{lineno}: {key}= 注释禁止含 {token!r}(面向用户,勿写开发文档指向/黑话)" + ) + break + return offenders + + def _rel(skill_root: str, path: str) -> str: return os.path.relpath(path, skill_root).replace("\\", "/") @@ -385,6 +473,23 @@ class TestPolicyConfig003(unittest.TestCase): ) +class TestPolicyConfig004(unittest.TestCase): + def test_env_example_comments_are_user_facing(self) -> None: + path = os.path.join(get_skill_root(), ".env.example") + self.assertTrue(os.path.isfile(path), msg=".env.example missing") + offenders = _config_004_comment_offenders(path) + self.assertEqual( + offenders, + [], + msg=_policy_msg( + "POLICY-CONFIG-004", + CONFIG_004_SOURCE, + "\n".join(offenders) + or "every active KEY in .env.example needs Chinese header+inline comments", + ), + ) + + class TestPolicyTesting001(unittest.TestCase): def test_root_tests_do_not_hardcode_real_targets(self) -> None: skill_root = get_skill_root()