From b50b16e6caa9f9e430e7d82cf3c0e33d384f5328 Mon Sep 17 00:00:00 2001 From: codex-layer-ci-maintainer Date: Fri, 16 Oct 2026 00:30:43 +0800 Subject: [PATCH] =?UTF-8?q?ci(gates):=20=E9=9B=86=E6=88=90=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=BC=93=E5=AD=98=E8=90=BD=E7=9B=98=E9=9A=94=E7=A6=BB?= =?UTF-8?q?=E9=97=A8=E7=A6=81=EF=BC=8C=E5=B9=B6=E8=A1=A5=E4=B8=8A=E7=AC=AC?= =?UTF-8?q?=E4=B8=89=E4=B8=AA=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 稳定层开启后 tools 回填缓存会落盘,任何以 prompt_prefix = true 启动中继子进程的 集成测试都会往真实状态目录写测试键。本机真的混进了一批 degrade-ok-*、 over-limit-stream-* 键,与真键同处一个文件,把「重启后第一个压缩轮是否命中预热」 这个真机判据搅浑。 - tests/native_compaction_wiring.rs:补注入 CODEX_LAYER_HOME 与 CODEX_LAYER_TOOLS_CACHE_PATH(此前只有另两个文件注入了,后来往这个文件加用例时 重犯了)。缓存变量单独注入而不是只改 HOME:它是独立覆盖变量,只改 HOME 时 一旦优先级链改动隔离就静默失效。 - 新增 scripts/ci/tools_cache_isolation_gate.py:判据是纯函数 audit_source, 源文件出现 prompt_prefix = true 即必须同时出现 CODEX_LAYER_TOOLS_CACHE_PATH。 带 --self-test 四例正反对照;扫不到测试文件或启用稳定层的用例数为 0 都显式失败, 不把「没文件可查」当成「全部合规」。 - 挂进 gates job 并写进贡献指南。这条必须进 CI:污染只出现在开发者本机, CI 原本完全看不见。 有效性反向验过:把注入撤掉,门禁立刻判红并点名到文件与缺失的变量名。 --- .github/workflows/ci.yml | 6 + CONTRIBUTING.md | 11 ++ scripts/ci/tools_cache_isolation_gate.py | 133 +++++++++++++++++++++++ tests/native_compaction_wiring.rs | 9 ++ 4 files changed, 159 insertions(+) create mode 100755 scripts/ci/tools_cache_isolation_gate.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc82196..9a56628 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,12 @@ jobs: env: GATE_PUSH_BEFORE: ${{ github.event.before }} run: python3 scripts/ci/new_file_fmt_gate.py + # 开稳定层的集成测试必须把 tools 回填缓存指到临时目录,否则 cargo test 会 + # 往真实状态目录写测试键(只污染开发者本机,CI 完全看不见,所以必须在此拦)。 + - name: 判据自测:集成测试缓存隔离 + run: python3 scripts/ci/tools_cache_isolation_gate.py --self-test + - name: 集成测试缓存隔离门禁 + run: python3 scripts/ci/tools_cache_isolation_gate.py # 安装脚本的 latest tag 解析:正反对照 + 旧写法恒空对照(离线跑,不碰网络) - name: 安装脚本 latest tag 解析测试 run: bash scripts/ci/install_tag_parse_test.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1e9509..b794eb6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -136,6 +136,7 @@ CI 的 `gates` job 里有两条判据,都可以本地先跑,不必等 CI: ```bash python3 scripts/ci/commit_author_gate.py --base origin/main # 提交作者角色身份 python3 scripts/ci/new_file_fmt_gate.py --base origin/main # 本次新增 *.rs 的格式 +python3 scripts/ci/tools_cache_isolation_gate.py # 开稳定层的测试是否隔离了缓存落盘 ``` - **提交作者**:区间内每个 commit 的 author 名必须是 `codex-layer-<角色>`, @@ -153,6 +154,16 @@ python3 scripts/ci/new_file_fmt_gate.py --base origin/main # 本次新增 *.r - **新增文件格式**:只判区间内**新增**的 `.rs`,既有文件的格式改动不误伤。 红了只格式化被点名的那一个文件(`rustfmt --edition 2024 <文件>`), **不许**全仓 `cargo fmt`。 +- **集成测试缓存隔离**:源文件里出现 `prompt_prefix = true` 即视为会启动开稳定层的 + 子进程,必须同时出现 `CODEX_LAYER_TOOLS_CACHE_PATH`。落盘上线后,未注入的测试会往 + **真实状态目录**写 e2e 键(`degrade-ok-*` 这类),真键与测试键混在一个文件里, + 「重启后第一个压缩轮是否命中预热」这个真机判据就废了。污染只出现在开发者本机、 + CI 看不见,所以这条必须在 CI 拦。新增用例时照既有三个文件的 spawn 处注入: + + ```rust + .env("CODEX_LAYER_HOME", dir.path()) + .env("CODEX_LAYER_TOOLS_CACHE_PATH", dir.path().join("tools-cache.json")) + ``` 两条判据都带 `--self-test`,先自证判据可用再判真实区间。若本地报「无法执行」 (取不到基线、找不到 rustfmt),那是判据自身不可用,会以非零退出, diff --git a/scripts/ci/tools_cache_isolation_gate.py b/scripts/ci/tools_cache_isolation_gate.py new file mode 100755 index 0000000..b95d550 --- /dev/null +++ b/scripts/ci/tools_cache_isolation_gate.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""集成测试隔离门禁:开稳定层的 spawn 必须把 tools 回填缓存指到临时目录。 + +立项依据:tools 回填缓存落盘(#69 第 6 条)之后,任何以 +`prompt_prefix = true` 启动中继子进程的集成测试都会**往真实状态目录写测试键**。 +本机真长出了一批 `degrade-ok-*`、`over-limit-stream-*` 之类的键, +把「重启后第一个压缩轮是否命中预热」这个真机判据搅浑——真键与测试键混在同一个文件里。 + +为什么做成门禁而不是靠人记住:注入这件事写在注释里挡不住新增测试, +每加一条开稳定层的用例都可能重犯,而犯了的信号只出现在开发者本机 +(真实目录被污染),CI 完全看不见。 + +判据(纯函数 `audit_source`,逐文件):源文件里出现 `prompt_prefix = true` +即视为会启动开稳定层的子进程,必须同时出现 `CODEX_LAYER_TOOLS_CACHE_PATH`。 +刻意只认这两个字面量、不做语法解析:两者都是 spawn 处必然手写的字符串, +漏任一个就说明隔离没做。 + +健康度约束同其他门禁:`--self-test` 必带正反对照;扫不到任何文件时显式失败, +不把「没文件可查」当成「全部合规」。 +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +import tempfile + +STABILIZER_ON = "prompt_prefix = true" +CACHE_ENV = "CODEX_LAYER_TOOLS_CACHE_PATH" + + +def audit_source(name: str, source: str) -> str | None: + """返回违规原因;合规返回 None。""" + if STABILIZER_ON not in source: + return None + if CACHE_ENV in source: + return None + return f"启用稳定层却未注入 {CACHE_ENV}(会写真实状态目录)" + + +def tracked_test_files(repo: str | None) -> list[str]: + proc = subprocess.run( + ("git", "ls-files", "tests/*.rs"), + capture_output=True, + text=True, + cwd=repo, + ) + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout or "").strip() + raise GateError(f"`git ls-files tests/*.rs` 失败(退出码 {proc.returncode}):{detail}") + return [line.strip() for line in proc.stdout.splitlines() if line.strip()] + + +class GateError(RuntimeError): + """判据自身不可用时抛出——绝不降级成「没有违规」。""" + + +def read(path: str) -> str: + with open(path, encoding="utf-8") as handle: + return handle.read() + + +def self_test() -> int: + cases = [ + ("启用 + 已注入", f"{STABILIZER_ON}\n.env({CACHE_ENV}, x)", None), + ("启用 + 未注入", f"{STABILIZER_ON}\n.stdin(null)", "违规"), + ("未启用", "prompt_prefix = false\n.stdin(null)", None), + ("空文件", "", None), + ] + for label, source, expect in cases: + reason = audit_source(label, source) + if expect is None and reason is not None: + print(f"自测失败:夹具「{label}」被判违规:{reason}", file=sys.stderr) + return 1 + if expect is not None and reason is None: + print(f"自测失败:夹具「{label}」应当判违规却全绿", file=sys.stderr) + return 1 + print( + f"隔离门禁自测通过:{len(cases)} 例夹具判定与期望一致" + "(启用+已注入放行、启用+未注入判红、未启用放行、空文件放行)。" + ) + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description="断言开稳定层的集成测试隔离了 tools 缓存落盘路径") + parser.add_argument("--self-test", action="store_true", help="跑夹具对照,不查真实仓库") + parser.add_argument("--repo", help="仓库目录(默认当前目录)") + args = parser.parse_args() + + if args.self_test: + return self_test() + + repo = args.repo or os.getcwd() + try: + files = tracked_test_files(repo) + except GateError as exc: + print(f"隔离门禁无法执行:{exc}", file=sys.stderr) + return 2 + if not files: + print("警告:没有跟踪任何 tests/*.rs,判据无事可做(不是「全部合规」)。", file=sys.stderr) + return 2 + + violations: list[str] = [] + enabled = 0 + for rel in files: + source = read(os.path.join(repo, rel)) + reason = audit_source(rel, source) + if STABILIZER_ON in source: + enabled += 1 + if reason: + violations.append(f" {rel}:{reason}") + print(f"隔离门禁:扫描 {len(files)} 个集成测试文件,其中启用稳定层的 {enabled} 个。") + if enabled == 0: + print("警告:没有任何启用稳定层的集成测试被检查,请确认判据是否失效。", file=sys.stderr) + return 2 + if violations: + print("发现未隔离的测试文件:", file=sys.stderr) + print("\n".join(violations), file=sys.stderr) + print( + "\n修正办法:在 spawn 子进程处注入独立路径,例如\n" + ' .env("CODEX_LAYER_TOOLS_CACHE_PATH", dir.path().join("tools-cache.json"))', + file=sys.stderr, + ) + return 1 + print("全部启用稳定层的集成测试都把 tools 缓存指到了临时目录。") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/native_compaction_wiring.rs b/tests/native_compaction_wiring.rs index e87ea87..49b6b21 100644 --- a/tests/native_compaction_wiring.rs +++ b/tests/native_compaction_wiring.rs @@ -171,6 +171,15 @@ path = "{}" .unwrap(); let child = Command::new(env!("CARGO_BIN_EXE_codex-layer")) .env("CODEX_LAYER_CONFIG", &config_path) + // 稳定层开启后 tools 回填缓存会落盘(#69 第 6 条):状态目录与缓存路径都要 + // 指到临时目录,否则 cargo test 会往真实状态目录写测试键,把「重启后第一个 + // 压缩轮是否命中预热」这个真机判据搅浑。缓存路径单独也注入一遍:它是独立 + // 的覆盖变量,只改 HOME 时旧二进制或改动优先级链都会让隔离失效。 + .env("CODEX_LAYER_HOME", log_path.parent().unwrap()) + .env( + "CODEX_LAYER_TOOLS_CACHE_PATH", + log_path.with_extension("tools-cache.json"), + ) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null())