Files
codex-app-server/cloud/tests/test_chv_arch95plus.py
T
host df8c64bd64 feat(chv): 架构/可维护性/前端只读侧推进——生产实验分离、配置片段化、VM 只读展示
架构:WarmPool 从 cloud/chv/pool.py 移到 experimental_pool.py,
生产残留清理彻底分文件到 cloud/chv/orphans.py(facade 直接读
registry 文件清理,不构造池、不碰 prewarm/acquire/release);
cloud/chv 与 cloud/sandbox 不再导出 WarmPool,旧路径一律
ImportError(CHV 未合并、无生产用户,兼容保留已无保留理由);
worker 改走 orphans facade,不再持有 _warm_pool;#215 对齐从
接口形状推进到 REPLACEMENT_POINTS 替换清单(R1/R2 可机械替换、
R3 永不替换,#215 模块当前不存在由测试钉住)。

可维护性:build_chv_config 拆成 base/ssh/network/quota/disk/
timeout 六片段(组装只做 dict 合并,键冲突即报错;38 字段集
与取值不变,单项非法报错文案逐字一致);ADR-12 模块职责图
与禁 import 清单(AST 钉住);cloud/chv/ 公开 API 全量类型
标注、无 Any 逃逸,并修正三处旧标注错误;死代码扫一遍
(删实验池已无调用的 _quarantine 委托与未用 _LOGGER)。

前端(只读侧,不做控制按钮):chv.ts 新增纯逻辑谓词与文案
(执行器精确匹配、附件守卫、VM 状态文案、列表徽标);列表
行新增 VM 已报告死亡徽标;详情对全部 CHV 任务显示 VM 行
(已报告死亡/未报告死亡,不猜 live);WorkersSection 与成本
卡沿用已有数据,不新开池管理/成本页;服务端未新增第二个
生命周期字段(持久化事实只有三项,chv_vm_dead 已是完整派生),
只把派生抽成 _chv_vm_dead_from_row 并补 goal 回落形态测试。
2026-10-31 06:55:26 +00:00

181 lines
7.0 KiB
Python

"""CHV 架构 9.5+ 提升轨测试(feat-chv-arch95plus,堆叠于 #252)。
只用标准库与 mock,绝不启动真实 VM;测试里只用 FAKE
占位密钥,绝无真实密钥。覆盖:长函数提取后的行为不变、
WarmPool facade 不调实验接口、#215 对齐接口形状、审计
失败计数不中断、只读字段 chv_vm_dead 的派生规则、
lockfile 校验脚本的通过/漂移/空清单。
"""
import json
import logging
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
try:
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
from cloud.chv.hardening import validate_snapshot_url
from cloud.chv.orphans import reconcile_orphans_for_executor
from cloud.sandbox import CloudHypervisorExecutor
from cloud.server import _task_to_dict
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
from cloud.chv.hardening import validate_snapshot_url
from cloud.chv.orphans import reconcile_orphans_for_executor
from cloud.sandbox import CloudHypervisorExecutor
from cloud.server import _task_to_dict
def _row(**kw):
base = {
"id": "t1", "project_id": "p1", "prompt": "x",
"status": "interrupted", "idempotency_key": None,
"depends_on": "[]", "priority": 0, "created_by": "u",
"created_at": 0, "updated_at": 0, "last_heartbeat": 0,
}
base.update(kw)
return base
class FakeExecutor:
def __init__(self):
self.acquire_called = False
self.prewarm_called = False
def shutdown_vm(self, vm):
return True
def _emit(self, *args, **kwargs):
pass
class Compat215Test(unittest.TestCase):
def test_resolve_inside_blocks_escape_and_symlink(self):
with tempfile.TemporaryDirectory() as d:
inside = os.path.join(d, "a.txt")
with open(inside, "w") as fh:
fh.write("x")
self.assertEqual(resolve_inside(d, "a.txt"), inside)
self.assertIsNone(resolve_inside(d, "../outside"))
self.assertIsNone(resolve_inside(d, "/etc/passwd"))
self.assertEqual(resolve_inside(d, ""), d)
def test_tmpfs_isolated_chv_and_unknown_fail_closed(self):
isolated, _ = tmpfs_isolated("cloud-hypervisor", "/tmp")
self.assertFalse(isolated)
isolated, _ = tmpfs_isolated("unknown-kind", "/tmp")
self.assertFalse(isolated)
def test_snapshot_url_uses_resolve_inside(self):
with tempfile.TemporaryDirectory() as d:
state = os.path.join(d, "state")
os.makedirs(state)
path = os.path.join(state, "s.raw")
with open(path, "w") as fh:
fh.write("x")
self.assertEqual(
validate_snapshot_url("file://" + path, state), path)
with self.assertRaises(Exception):
validate_snapshot_url(
"file:///etc/passwd", state)
def test_no_timeout_or_quota_map(self):
import cloud.chv.compat215 as m
self.assertFalse(hasattr(m, "resolve_module_timeout"))
self.assertFalse(hasattr(m, "decide"))
class PoolFacadeTest(unittest.TestCase):
def test_facade_only_reconciles_never_acquires(self):
# facade 直接读 registry 文件做清理(本轨不再构造
# WarmPool):用假执行器 + 空 registry,返回 [] 不炸,
# 且绝不调 acquire/prewarm(假执行器根本没有这两个方法)。
ex = FakeExecutor()
# facade 需要 state_dir 属性定 registry 路径;没有则返回 []
ex.state_dir = tempfile.mkdtemp()
ex.warm_pool_size = 0
ex.snapshot_url = None
result = reconcile_orphans_for_executor(ex)
self.assertEqual(result, [])
self.assertFalse(ex.acquire_called)
self.assertFalse(ex.prewarm_called)
class AuditCounterTest(unittest.TestCase):
def test_emit_failure_counts_and_does_not_raise(self):
ex = CloudHypervisorExecutor.__new__(CloudHypervisorExecutor)
ex.audit_callback = lambda *a, **k: (_ for _ in ()).throw(
RuntimeError("db down"))
ex.audit_failure_count = 0
# _emit 不抛异常(任务不因此中断),计数 +1
ex._emit("vm_boot", {"id": "t1"}, "vm=chv-t1")
self.assertEqual(ex.audit_failure_count, 1)
ex._emit("vm_boot", {"id": "t1"}, "vm=chv-t1")
self.assertEqual(ex.audit_failure_count, 2)
class ChvVmDeadFieldTest(unittest.TestCase):
def test_only_chv_with_vm_death_reason_returns_true(self):
row = _row(executor="cloud-hypervisor",
interrupted_reason="VM 中途死亡,任务未完成")
d = _task_to_dict(row)
self.assertTrue(d.get("chv_vm_dead"))
row2 = _row(executor="cloud-hypervisor",
interrupted_reason="quota")
self.assertNotIn("chv_vm_dead", _task_to_dict(row2))
row3 = _row(executor="docker",
interrupted_reason="VM 中途死亡,任务未完成")
self.assertNotIn("chv_vm_dead", _task_to_dict(row3))
row4 = _row(executor="cloud-hypervisor",
interrupted_reason=None)
self.assertNotIn("chv_vm_dead", _task_to_dict(row4))
class LockfileScriptTest(unittest.TestCase):
def _run(self, data, extra=None):
with tempfile.TemporaryDirectory() as d:
p = os.path.join(d, "BUILDINFO.json")
with open(p, "w") as fh:
json.dump(data, fh)
script = str(Path(__file__).resolve().parents[2]
/ "chv-guest" / "check_lockfile.py")
cmd = [sys.executable, script, p] + (extra or [])
return subprocess.run(cmd, capture_output=True, text=True)
def test_pass_and_empty_and_drift(self):
good = {"tag": "t", "versions": {"dpkg": ["a 1"],
"pip": ["b==2"]}}
self.assertEqual(self._run(good, ["--tag", "t"]).returncode, 0)
bad = {"tag": "t", "versions": {"dpkg": [], "pip": []}}
self.assertNotEqual(self._run(bad).returncode, 0)
with tempfile.TemporaryDirectory() as d:
frozen = os.path.join(d, "frozen.json")
with open(frozen, "w") as fh:
json.dump({"dpkg": ["missing 9"], "pip": []}, fh)
self.assertNotEqual(
self._run(good, ["--frozen", frozen]).returncode, 0)
class LongFunctionTest(unittest.TestCase):
def test_no_chv_function_over_50_lines(self):
import ast
base = Path(__file__).resolve().parents[2] / "cloud" / "chv"
over = []
for f in base.glob("*.py"):
tree = ast.parse(f.read_text())
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
n = node.end_lineno - node.lineno + 1
if n > 50:
over.append("%s:%s:%d" % (f.name, node.name, n))
self.assertEqual(over, [])
if __name__ == "__main__":
unittest.main()