Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<角色>`,
Expand All @@ -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),那是判据自身不可用,会以非零退出,
Expand Down
133 changes: 133 additions & 0 deletions scripts/ci/tools_cache_isolation_gate.py
Original file line number Diff line number Diff line change
@@ -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())
9 changes: 9 additions & 0 deletions tests/native_compaction_wiring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading