Files
codex-app-server/cloud/tests/test_harness2.py
T
host f39ed3654e ci3: lint/mypy 第二阶段清理 + 覆盖率 76% -> 78%(门槛不动)
基底:#251 头 174b631(堆叠于 #242 feat-ci-expand 之上),
基线 origin/main aebc6238;本轨不改 #251 已定语义、不动
workflow 门槛、不删检查、不加 skip。

lint(ruff E,F@120):109 -> 10
- F401 43 -> 0:删未用导入;try/except 双绑定的名字出现
  “打地鼠”(删 except 分支后 try 分支又被标出),两分支
  均已核对正文无引用后清掉;MCP 三模块的 Error 名不删,
  改走 importlib 属性绑定(见下),两路径绑定都保留。
- E702 40 -> 0:全在 host.py main() 的 argparse 段,纯分行,
  tokenize 按分号 token 拆(含 2377 行多行语句只断首个分号);
  一次 5 空格缩进失误已回滚重做并以 ast + 全量 host 测试验证。
- E501 13 -> 0:host.py 5 项随 E702 分行连带清掉;mcp_server
  3 项把描述拆成相邻字面量(AST 比对 24 条工具描述与原值
  完全一致,注意 ruff 按东亚宽度计长,CJK 算 2);test_host
  5 项断开 git commit 参数表。
- E402 8 -> 10:生产侧 2 项清掉(db.py _re、github_app
  _base64 移到顶部,之前无引用、无重名);剩余 10 项全是
  tests/ 的 sys.path 前置 import(6 项旧 + 4 个新测试文件
  沿用同一既定模式),移上去会破坏单文件直跑,有意保留。
- F841 4 -> 0:只去绑定、不删调用(run_task、DB 入队、
  tarfile 上下文、git 提交),各有注释与原测试/新测试覆盖;
  host dispatch_from_branch 新增真实 git 仓库回归测试。
- F811 1 -> 0:server 本地 _row_get 与 peer_messages.row_get
  逐行相同,删本地定义,全部调用点解析到同一 import;
  新增 test_server_row_get(同一对象 + 三路行为)。

mypy(生产范围):24 -> 0,零新增 type: ignore
- no-redef 9 -> 0:MCP 三模块 try/except 两次 from-import
  改 importlib.import_module + 属性绑定(cloud 优先、flat
  copy 回落、内层 ImportError 同样回落,语义与原来一致;
  flat-copy 子进程测试证实回落后 __module__ 为顶层名);
  server _row_get 随 F811 消除。
- arg-type 10 -> 0:harness2_tasks shared 改 TypedDict
  逐键精确类型;CodexProbeAdapter 补 no-op validate_tree
  (_adapter_validate 本就跳过缺失方法,行为一致)。
- var-annotated 3 -> 0:sub2api _SHARED_ADMIN/_login_slots、
  verify-multiuser RESULTS 加精确注解。
- assignment/misc 2 -> 0:marker_tools ZoneInfo 先声明
  Any 再 try import(草稿先验 mypy 通过)。

bandit:总数 286 不变,High 仍 4 项,全在 host.py,本轨不改
(属第 3 项白名单轨范围),只做 triage:
- 1319 run_verify:无插值,整串验测命令,管道/&& 须 shell
  语义(新增 test_host_run_verify 断言管道与 && 真实生效)。
- 1497 notify:模板来自算子配置,三处插值全部 shlex.quote,
  证据为既有 test_notify_failure_quotes_shell_metacharacters
  (; touch 与 $(touch ) 不执行)。
- 2099/2103 goal verify:无插值,任务配置验测串,同 run_verify。
- 无新增 High;pip-audit 0 漏洞。

覆盖率(同口径三套件 coverage 计量):75.96% -> 77.87%
(22392 行/缺 5385 -> 22437 行/缺 4963,显示 76% -> 78%)
- client.py 0% -> 90%(pipe 假子进程 + main 分支)
- run_parallel.py 0% -> 97%、migrate 包装 0% -> 95%
- playwright_mcp 37% -> 62%、imagegen_mcp 47% -> 79%、
  websearch_mcp 51% -> 86%、deploy.py 56% -> 77%
- verify-multiuser 0% -> 27%(仅 check/req helper;main()
  是主机侧真实多用户实弹验证,按门禁不建生产任务、
  不以 mock 冒充,仍为缺口)
- host.py 55% -> 57%(文件 2020 -> 2060 行系 E702 分行
  的计量口径变化,非代码膨胀)
- 未到 85%:门槛保持 75% 不动,不删低覆盖模块出统计口径;
  缺口主力仍是 server.py / worker.py / host.py 大文件,
  下一阶段须针对性深测,非本轨快赢可及。

验证:三套件 1576 -> 1629 项全过(tests 127、cloud/tests
1449 skipped=4、deploy/tests 53);ruff E9 通过;mypy 0;
bandit High 4(同上);pip-audit 0 漏洞;试跑产物已删,
提交后工作树干净。
2026-11-03 02:48:54 +00:00

332 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""cloud/harness2.py 单测:递归改进循环。
不跑真实模型、不联网:执行器在进程内跑 harness 树里的脚本,
编辑器/选择器/反思器都是确定性假实现(上游 harness2-demo 的同款
做法:base 脚本只加正数,编辑器去掉正数过滤,正确总数是带符号和)。
账本:100.25, -20.10, 5.00 —— 带符号和 85.15,只加正数是 105.25。
"""
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
try:
from cloud.harness2 import (
Library, Rollout, EditResult, validate,
read_tree, write_tree, is_allowed, normalize_rel, autotrigger,
diff_summary, diversity, improve_task, select_final,
assign_ids,
)
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.harness2 import (
Library, Rollout, EditResult, validate,
read_tree, write_tree, is_allowed, normalize_rel, autotrigger,
diff_summary, diversity, improve_task, select_final,
assign_ids,
)
BASE_SCRIPT = """import csv
import sys
from decimal import Decimal
with open(sys.argv[1]) as f:
amounts = [Decimal(r['amount']) for r in csv.DictReader(f)]
print(f"Total: {sum((a for a in amounts if a > 0), Decimal(0)):.2f}")
"""
BASE_TREE = {
"AGENTS.md": "Use scripts/summarize.py to total the ledger.\n",
"scripts/summarize.py": BASE_SCRIPT,
}
class LedgerAdapter:
"""任务:对账本求和。执行 = 真跑 harness 树里的脚本(无模型)。"""
task_id = "ledger-1"
domain = "accounting"
prompt = "Sum every signed amount in ledger.csv. Return Total: with two decimals."
def __init__(self, root):
self.root = Path(root)
self.executions = 0
self.ledger = self.root / "ledger.csv"
self.ledger.write_text("amount\n100.25\n-20.10\n5.00\n", encoding="utf-8")
def base_harness(self):
return dict(BASE_TREE)
def execute(self, label, harness):
self.executions += 1
work = self.root / "runs" / label
work.mkdir(parents=True, exist_ok=True)
write_tree(harness, work / "harness")
proc = subprocess.run(
[sys.executable, str(work / "harness" / "scripts" / "summarize.py"),
str(self.ledger)],
capture_output=True, text=True, timeout=10,
)
out = proc.stdout.strip()
return Rollout(
label=label, ok=proc.returncode == 0, delivered=out.startswith("Total:"),
deliverable_text=out, trajectory="[ACTION] Run scripts/summarize.py\n"
"[OBSERVATION]\n" + out, cost_usd=0.01,
error="" if proc.returncode == 0 else proc.stderr[-200:],
)
def validate_tree(self, harness):
if "scripts/summarize.py" not in harness:
raise ValueError("The local executor requires scripts/summarize.py")
class FixEditor:
"""确定性编辑器:去掉正数过滤;同时在 context/harness/ 里真实改文件,
模拟 codex 任务编辑工作目录的方式,再把改完的树读回。"""
def edit(self, mission, context_dir, label):
harness_dir = Path(context_dir) / "harness"
script = harness_dir / "scripts" / "summarize.py"
script.write_text(script.read_text().replace(" if a > 0", ""), encoding="utf-8")
return EditResult(tree=read_tree(harness_dir),
notes="Keep signed amounts: the base total omitted credits.")
class BadEditor:
"""永远交回非法树:AGENTS.md 为空,校验必拒,最终必须回落到 base。"""
def edit(self, mission, context_dir, label):
return EditResult(tree={"AGENTS.md": ""}, notes="bad proposal")
class PickSignedSelector:
def select(self, mission, context_dir):
for path in sorted((Path(context_dir) / "candidates").glob("*.md")):
if "Total: 85.15" in path.read_text():
return path.stem, "The signed total matches the source ledger.", 0.0
raise ValueError("no candidate with the signed total")
class PickFirstSelector:
def select(self, mission, context_dir):
paths = sorted((Path(context_dir) / "candidates").glob("*.md"))
return paths[0].stem, "first delivered candidate", 0.0
class RaisingSelector:
def select(self, mission, context_dir):
raise RuntimeError("selector session failed")
class LedgerReflector:
def reflect(self, mission, context_dir):
root = Path(context_dir)
changed = (root / "harness" / "scripts" / "summarize.py").read_text() != (
root / "base_harness" / "scripts" / "summarize.py").read_text()
if not changed:
return {}
return {
"cue": "When a ledger includes credits and debits",
"lesson": "Include both signs when summing in scripts/summarize.py.",
"evidence": "Removing the positive-only filter changed the probe total "
"to include the credits omitted by the base trajectory.",
"confidence": "high for this observed arithmetic contrast",
"edits": "scripts/summarize.py: dropped the positive-only filter; observed: "
"probe total included the omitted credits; verdict: helped",
}
class TestTree(unittest.TestCase):
def test_validate_accepts_base(self):
census = validate(dict(BASE_TREE))
self.assertEqual(census["scripts"], 1)
self.assertEqual(census["n_files"], 2)
def test_validate_rejects_skill_without_description(self):
tree = dict(BASE_TREE)
tree[".agents/skills/check/SKILL.md"] = "Do the check.\n"
tree["AGENTS.md"] += "- When checking, read .agents/skills/check/SKILL.md.\n"
with self.assertRaises(ValueError) as cm:
validate(tree)
self.assertIn("description", str(cm.exception))
def test_validate_rejects_untriggered_skill(self):
tree = dict(BASE_TREE)
tree[".agents/skills/check/SKILL.md"] = "---\ndescription: check totals\n---\nDo it.\n"
with self.assertRaises(ValueError) as cm:
validate(tree)
self.assertIn("never named", str(cm.exception))
def test_validate_rejects_bad_hooks_and_bad_script(self):
tree = dict(BASE_TREE)
tree[".codex/hooks.json"] = '{"hooks": {"Nope": []}}'
with self.assertRaises(ValueError):
validate(tree)
tree = dict(BASE_TREE)
tree["scripts/summarize.py"] = "def broken(:\n"
with self.assertRaises(ValueError):
validate(tree)
tree = dict(BASE_TREE)
tree["AGENTS.md"] = "x" * 8001
with self.assertRaises(ValueError):
validate(tree)
def test_validate_rejects_missing_script_ref(self):
tree = dict(BASE_TREE)
tree["AGENTS.md"] += "Run python3 scripts/nope.py first.\n"
with self.assertRaises(ValueError):
validate(tree)
def test_autotrigger_repairs_and_respects_cap(self):
tree = dict(BASE_TREE)
tree[".agents/skills/check/SKILL.md"] = "---\ndescription: check totals\n---\nDo it.\n"
fixed = autotrigger(tree)
self.assertEqual(fixed, ["check"])
self.assertIn("check", tree["AGENTS.md"])
validate(tree) # 补完触发行后应能通过
capped = dict(BASE_TREE)
capped["AGENTS.md"] = "x" * 7995
capped[".agents/skills/check/SKILL.md"] = "---\ndescription: check totals\n---\nDo it.\n"
self.assertEqual(autotrigger(capped), []) # 超上限时跳过,不硬塞
def test_paths_and_roundtrip(self):
with self.assertRaises(ValueError):
normalize_rel("../escape.py")
self.assertFalse(is_allowed("/etc/passwd"))
self.assertFalse(is_allowed("evil.py"))
self.assertTrue(is_allowed(".agents/skills/a/SKILL.md"))
with tempfile.TemporaryDirectory() as tmp:
dest = Path(tmp) / "tree"
write_tree(dict(BASE_TREE), dest)
self.assertEqual(read_tree(dest), BASE_TREE)
rejected = []
(dest / "evil.py").write_text("x", encoding="utf-8")
self.assertEqual(read_tree(dest, rejected=rejected), BASE_TREE)
self.assertEqual(rejected, ["evil.py"])
def test_diff_and_diversity(self):
changed = dict(BASE_TREE)
changed["scripts/summarize.py"] = "changed"
d = diff_summary(BASE_TREE, changed)
self.assertEqual(d["modified"], ["scripts/summarize.py"])
self.assertEqual(d["added"], [])
div = diversity({"a": BASE_TREE, "b": changed})
self.assertEqual(div["n_distinct"], 2)
self.assertLess(div["mean_line_jaccard"], 1.0)
class TestImprove(unittest.TestCase):
def _run(self, mode, editor=None, selector=None, budget=None, k=1):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
root = Path(tmp.name)
adapter = LedgerAdapter(root)
library = Library(root / "library", "accounting")
result = improve_task(
adapter, editor or FixEditor(),
selector=selector or PickSignedSelector(),
reflector=LedgerReflector(), work_dir=root / "work",
k=k, mode=mode, library=library, budget_usd=budget)
return result, adapter, library, root
def test_parallel_k1_improves_and_enters_library(self):
result, adapter, library, root = self._run("parallel")
self.assertTrue(result.ok)
self.assertEqual(result.selection.chosen_label, "agg1")
chosen = result.mode_result.rollouts["agg1"]
self.assertEqual(chosen.deliverable_text, "Total: 85.15")
self.assertEqual(adapter.executions, 4) # base + inv1 + dom1 + agg1
self.assertIsNotNone(result.library_entry)
self.assertIn("ledger-1", library.entries())
index = (root / "library" / "accounting" / "INDEX.md").read_text()
self.assertIn("credits and debits", index)
self.assertIn("85.15", result.selection.why + chosen.deliverable_text)
def test_sequential_k1_improves(self):
result, adapter, library, root = self._run("sequential")
self.assertTrue(result.ok)
self.assertEqual(result.selection.chosen_label, "c1")
self.assertEqual(result.mode_result.rollouts["c1"].deliverable_text, "Total: 85.15")
self.assertEqual(adapter.executions, 4) # base + inv1 + dom1 + c1
def test_invalid_proposal_falls_back_to_base(self):
result, adapter, library, root = self._run(
"parallel", editor=BadEditor(), selector=PickFirstSelector())
self.assertTrue(result.ok) # 回落的 base 产物仍可交付
self.assertEqual(set(result.mode_result.fell_back), {"inv1", "dom1", "agg1"})
self.assertEqual(result.mode_result.trees["agg1"], BASE_TREE)
self.assertIsNone(result.library_entry) # 树没变,反思无经验,不入库
def test_budget_exceeded_blocks_library_but_still_delivers(self):
result, adapter, library, root = self._run("parallel", budget=0.03)
self.assertTrue(result.ok)
self.assertTrue(result.budget_exceeded)
self.assertIsNone(result.library_entry)
def test_k_validation(self):
with tempfile.TemporaryDirectory() as tmp:
adapter = LedgerAdapter(Path(tmp))
with self.assertRaises(ValueError):
improve_task(adapter, FixEditor(), work_dir=Path(tmp) / "w", k=0)
with self.assertRaises(ValueError):
improve_task(adapter, FixEditor(), work_dir=Path(tmp) / "w", k=4)
with self.assertRaises(ValueError):
improve_task(adapter, FixEditor(), work_dir=Path(tmp) / "w", k=1,
mode="sideways")
def test_trivial_k1_without_selector(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
root = Path(tmp.name)
adapter = LedgerAdapter(root)
result = improve_task(adapter, FixEditor(), selector=None,
reflector=LedgerReflector(), work_dir=root / "work",
k=1, mode="parallel")
self.assertTrue(result.ok)
self.assertTrue(result.selection.trivial)
class TestSelection(unittest.TestCase):
def test_drops_undelivered_and_falls_back_on_selector_error(self):
with tempfile.TemporaryDirectory() as tmp:
adapter = LedgerAdapter(Path(tmp))
rollouts = {
"agg1": Rollout(label="agg1", ok=True, delivered=True,
deliverable_text="Total: 1.00"),
"agg2": Rollout(label="agg2", ok=False, delivered=False),
}
# k=2 的终版是 agg1/agg2;agg2 未交付应被丢掉,agg1 是唯一候选
sel = select_final("ledger-1", 2, "parallel", rollouts, adapter=adapter,
selector=RaisingSelector(), work_dir=Path(tmp) / "work",
base_tree=dict(BASE_TREE))
self.assertTrue(sel.ok)
self.assertTrue(sel.fallback)
self.assertEqual(sel.chosen_label, "agg1")
self.assertIn("agg2", sel.dropped)
def test_assign_ids_is_stable(self):
a = assign_ids(["agg2", "agg1"], task_id="t", k=1, mode="parallel")
b = assign_ids(["agg1", "agg2"], task_id="t", k=1, mode="parallel")
self.assertEqual(a, b) # 输入顺序不影响匿名映射
class TestLibrary(unittest.TestCase):
def test_add_requires_experience(self):
with tempfile.TemporaryDirectory() as tmp:
lib = Library(Path(tmp) / "lib", "accounting")
self.assertIsNone(lib.add("t1", dict(BASE_TREE), experience={}))
self.assertEqual(lib.entries(), [])
self.assertIsNone(lib.context_dir())
entry = lib.add("t1", dict(BASE_TREE), why="why", label="agg1",
experience={"cue": "c", "lesson": "l", "evidence": "e"})
self.assertIsNotNone(entry)
self.assertEqual(lib.entries(), ["t1"])
self.assertEqual(lib.tree_of("t1"), BASE_TREE)
self.assertIsNotNone(lib.context_dir())
if __name__ == "__main__":
unittest.main()