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 回落形态测试。
This commit is contained in:
host committed 2026-10-31 06:55:26 +00:00
1 parent 42de20da68
commit df8c64bd64
30 files changed
+1263 -331

No files matched your search

+464
View File
@@ -0,0 +1,464 @@
"""CHV 架构/可维护性/前端(只读侧)推进轨测试。
本轨 feat-chv-arch-maint-fe,堆叠于 #254(feat-chv-
arch95plus)。只用标准库与 mock,绝不启动真实 VM;
测试里只用 FAKE 占位值,绝无真实密钥。覆盖:
- WarmPool 破环性清理:旧 import 路径一律 ImportError,
实验路径唯一,生产代码不 import 实验模块;
- orphans facade:registry 语义(有效/非法/损坏/缺失/
shutdown 抛错/_emit 抛错)与旧 WarmPool 逐项一致;
- 配置片段:五片段键集互不相交、并集等于旧字段集、
默认值与单项非法报错逐字一致、组装键冲突即报错;
- ADR-12 模块依赖图:叶子模块只依赖标准库、禁 import
清单、公开 API 全量类型标注且无 Any 逃逸;
- #215 替换清单:每项 local 真实存在、#215 模块当前
不存在、R3 永不替换;
- 服务端只读字段:goal 回落形态(纯文本“VM 中途死亡”)
同样派生 chv_vm_dead,非 VM 中断原因一律不派生。
"""
import ast
import importlib.util
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
try:
from cloud.chv import reconcile_orphans_for_executor
from cloud.chv.compat215 import REPLACEMENT_POINTS
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
from cloud.chv.config import _assemble_chv_config
from cloud.chv.config import _base_fragment
from cloud.chv.config import _disk_fragment
from cloud.chv.config import _network_fragment
from cloud.chv.config import _quota_fragment
from cloud.chv.config import _ssh_fragment
from cloud.chv.config import _timeout_fragment
from cloud.chv.config import build_chv_config
from cloud.chv.experimental_pool import WarmPool
from cloud.chv.orphans import _REGISTRY_FILENAME
from cloud.chv.orphans import reconcile_orphans_for_executor as _facade
from cloud.server import _chv_vm_dead_from_row, _task_to_dict
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.chv import reconcile_orphans_for_executor
from cloud.chv.compat215 import REPLACEMENT_POINTS
from cloud.chv.compat215 import resolve_inside, tmpfs_isolated
from cloud.chv.config import _assemble_chv_config
from cloud.chv.config import _base_fragment
from cloud.chv.config import _disk_fragment
from cloud.chv.config import _network_fragment
from cloud.chv.config import _quota_fragment
from cloud.chv.config import _ssh_fragment
from cloud.chv.config import _timeout_fragment
from cloud.chv.config import build_chv_config
from cloud.chv.experimental_pool import WarmPool
from cloud.chv.orphans import _REGISTRY_FILENAME
from cloud.chv.orphans import reconcile_orphans_for_executor as _facade
from cloud.server import _chv_vm_dead_from_row, _task_to_dict
ROOT = Path(__file__).resolve().parents[2]
CHV = ROOT / "cloud" / "chv"
EXPECTED_FIELDS = {
"binary", "kernel", "rootfs", "cpus", "memory", "state_dir",
"kvm_path", "cpuinfo_path", "ch_remote", "model",
"ssh_key", "ssh_user", "ssh_port", "host_key_mode",
"guest_host_py", "cmdline",
"net", "net_json", "ssh_host", "ssh_forward",
"egress_proxy", "allow_direct_egress",
"max_vms", "warm_pool_size",
"disk_mode", "disk_gb", "reuse_experiment",
"single_tenant_project", "snapshot_max_count",
"snapshot_max_total_gb", "snapshot_retention_days",
"snapshot_dir", "allow_remote_snapshot", "snapshot_url",
"task_timeout", "api_timeout", "ready_timeout", "ready_interval",
}
class FakeProdExecutor:
"""生产 facade 替身:只有 facade 允许碰的属性。
acquire/prewarm/release 一律不存在——facade 若敢碰,
__getattr__ 直接抛错,测试即 fail。"""
def __init__(self, state_dir):
self.state_dir = state_dir
self.shutdowns = []
self.emits = []
def __getattr__(self, name):
if name in ("acquire", "prewarm", "release"):
raise AssertionError("facade 不得碰实验接口:%s" % name)
raise AttributeError(name)
def shutdown_vm(self, vm):
self.shutdowns.append(vm)
return True
def _emit(self, event, task, detail):
self.emits.append(event)
def _write_registry(state_dir, data):
path = os.path.join(state_dir, _REGISTRY_FILENAME)
if isinstance(data, str):
with open(path, "w") as fh:
fh.write(data)
else:
with open(path, "w") as fh:
json.dump(data, fh)
return path
class BreakingCleanupTest(unittest.TestCase):
def test_old_paths_raise_import_error(self):
with self.assertRaises(ImportError):
from cloud.chv import WarmPool # noqa: F401
with self.assertRaises(ImportError):
from cloud.sandbox import WarmPool # noqa: F401
with self.assertRaises(ImportError):
import cloud.chv.pool # noqa: F401
def test_new_paths_and_identity(self):
self.assertEqual(WarmPool.__module__,
"cloud.chv.experimental_pool")
self.assertIs(reconcile_orphans_for_executor, _facade)
import cloud.sandbox as sb
self.assertIs(sb.reconcile_orphans_for_executor, _facade)
self.assertFalse(WarmPool.wired_to_dispatch)
def test_production_sources_never_import_experimental(self):
# 钉的是 import 语句,不是文档提及:executor/__init__
# 的 docstring 本来就要写清实验模块去向,文本匹配会
# 误杀,故用 AST 只看 Import/ImportFrom 节点。
for rel in ("cloud/worker.py", "cloud/chv/executor.py",
"cloud/chv/__init__.py", "cloud/sandbox.py",
"cloud/chv/orphans.py"):
tree = ast.parse((ROOT / rel).read_text())
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for a in node.names:
self.assertNotIn(a.name, (
"cloud.chv.pool",
"cloud.chv.experimental_pool"), rel)
if isinstance(node, ast.ImportFrom):
self.assertNotIn(node.module, (
"cloud.chv.pool",
"cloud.chv.experimental_pool"), rel)
if node.module in ("cloud.chv", "cloud.sandbox"):
names = [a.name for a in node.names]
self.assertNotIn("WarmPool", names, rel)
class OrphansTest(unittest.TestCase):
def test_valid_warm_and_assigned_cleaned_invalid_dropped(self):
with tempfile.TemporaryDirectory() as d:
path = _write_registry(d, {
"a": {"state": "warm", "vm": "chv-a",
"task_id": "a", "snapshot_url": None},
"b": {"state": "assigned", "vm": "chv-b",
"task_id": "b", "snapshot_url": None},
"c": {"state": "warm", "task_id": "c"},
"e": {"state": "running", "vm": "chv-e"},
"f": "not-a-dict",
})
ex = FakeProdExecutor(d)
self.assertEqual(
reconcile_orphans_for_executor(ex),
["chv-a", "chv-b"])
self.assertEqual(ex.shutdowns, ["chv-a", "chv-b"])
self.assertEqual(ex.emits, ["warm_pool_reconcile"])
with open(path) as fh:
self.assertEqual(json.load(fh), {})
def test_invalid_only_returns_empty_and_leaves_file(self):
with tempfile.TemporaryDirectory() as d:
path = _write_registry(d, {
"c": {"state": "warm", "task_id": "c"}})
ex = FakeProdExecutor(d)
self.assertEqual(reconcile_orphans_for_executor(ex), [])
self.assertEqual(ex.shutdowns, [])
with open(path) as fh:
self.assertIn("task_id", fh.read())
def test_corrupt_json_quarantined(self):
with tempfile.TemporaryDirectory() as d:
path = _write_registry(d, "{not json")
ex = FakeProdExecutor(d)
self.assertEqual(reconcile_orphans_for_executor(ex), [])
self.assertFalse(os.path.exists(path))
self.assertTrue(os.path.exists(path + ".corrupt"))
def test_nondict_top_level_quarantined(self):
with tempfile.TemporaryDirectory() as d:
path = _write_registry(d, "[1, 2]")
ex = FakeProdExecutor(d)
self.assertEqual(reconcile_orphans_for_executor(ex), [])
self.assertTrue(os.path.exists(path + ".corrupt"))
def test_missing_file_and_no_state_dir(self):
with tempfile.TemporaryDirectory() as d:
ex = FakeProdExecutor(d)
self.assertEqual(reconcile_orphans_for_executor(ex), [])
class NoDir:
def shutdown_vm(self, vm):
raise AssertionError("must not be called")
self.assertEqual(
reconcile_orphans_for_executor(NoDir()), [])
def test_shutdown_and_emit_failures_do_not_break_cleanup(self):
with tempfile.TemporaryDirectory() as d:
_write_registry(d, {
"a": {"state": "warm", "vm": "chv-a"}})
ex = FakeProdExecutor(d)
def boom(vm):
raise RuntimeError("shutdown down")
ex.shutdown_vm = boom
ex._emit = lambda *a, **k: (_ for _ in ()).throw(
RuntimeError("audit down"))
self.assertEqual(
reconcile_orphans_for_executor(ex), ["chv-a"])
def test_experimental_pool_registry_roundtrip_unchanged(self):
# 实验池自身的 registry 语义(载入即 orphan、reconcile
# 清理)与上一轨一致,只是文件 IO 委托给 orphans。
with tempfile.TemporaryDirectory() as d:
ex = FakeProdExecutor(d)
ex.warm_pool_size = 0
ex.snapshot_url = None
pool1 = WarmPool(ex)
pool1._entries["chv-old"] = {
"state": "warm", "vm": "chv-old",
"task": {"id": "old"}, "snapshot_url": None}
pool1._save_registry()
pool2 = WarmPool(ex)
self.assertEqual(pool2.orphan_count(), 1)
self.assertEqual(pool2.warm_count(), 0)
self.assertEqual(pool2.reconcile_orphans(), ["chv-old"])
class ConfigFragmentsTest(unittest.TestCase):
def test_fragment_keys_disjoint_and_union_is_field_set(self):
frags = [_base_fragment({}, {}), _ssh_fragment({}, {}),
_network_fragment({}, {}), _quota_fragment({}),
_disk_fragment({}), _timeout_fragment({}, {})]
seen = set()
for frag in frags:
self.assertFalse(seen & set(frag), "片段键重叠")
seen |= set(frag)
self.assertEqual(seen, EXPECTED_FIELDS)
self.assertEqual(len(EXPECTED_FIELDS), 38)
def test_defaults_and_overrides(self):
cfg = build_chv_config({}, {})
self.assertEqual(cfg.binary, "cloud-hypervisor")
self.assertEqual(cfg.ssh_port, 2222)
self.assertEqual(cfg.ssh_user, "root")
self.assertEqual(cfg.host_key_mode, "provisioned")
self.assertEqual(cfg.max_vms, 4)
self.assertEqual(cfg.disk_mode, "copy")
self.assertEqual(cfg.disk_gb, 50.0)
self.assertEqual(cfg.api_timeout, 30.0)
self.assertEqual(cfg.ready_timeout, 120.0)
self.assertEqual(cfg.ready_interval, 5.0)
self.assertIsNone(cfg.task_timeout)
self.assertIsNone(cfg.snapshot_url)
cfg2 = build_chv_config(
{"ssh_user": "u", "ssh_port": 2223},
{"CHV_SSH_USER": "env-user", "CHV_MAX_VMS": "2",
"CHV_EGRESS_PROXY": "http://p:1",
"CHV_SNAPSHOT_URL": "file:///snap/base"})
self.assertEqual(cfg2.ssh_user, "u")
self.assertEqual(cfg2.ssh_port, 2223)
self.assertEqual(cfg2.max_vms, 2)
self.assertEqual(cfg2.egress_proxy, "http://p:1")
self.assertEqual(cfg2.snapshot_url, "file:///snap/base")
def test_single_error_messages_unchanged(self):
cases = [
({"ssh_user": ""}, {}, "CHV_SSH_USER 不能为空字符串"),
({}, {"CHV_SSH_PORT": "0"}, "CHV_SSH_PORT 必须在 1-65535 之间"),
({}, {"CHV_SSH_PORT": "99999"},
"CHV_SSH_PORT 必须在 1-65535 之间"),
({}, {"CHV_DISK_MODE": "bad"},
"CHV_DISK_MODE 只能是 copy 或 reuse"),
({}, {"CHV_DISK_GB": "0"}, "CHV_DISK_GB 必须大于 0"),
({}, {"CHV_SNAPSHOT_MAX_COUNT": "-1"},
"CHV_SNAPSHOT_* 不能为负数"),
({}, {"CHV_MAX_VMS": "x"}, "CHV_MAX_VMS 必须是整数"),
({}, {"CHV_WARM_POOL_SIZE": "x"},
"CHV_WARM_POOL_SIZE 必须是整数"),
({}, {"CHV_API_TIMEOUT": "x"},
"CHV_*_TIMEOUT/INTERVAL 必须是数字"),
]
for params, env, msg in cases:
with self.assertRaises(ValueError) as cm:
build_chv_config(params, env)
self.assertIn(msg, str(cm.exception))
def test_assemble_key_conflict_raises(self):
with self.assertRaises(ValueError):
_assemble_chv_config([{"a": 1}, {"a": 2}])
def _top_level_imports(path):
tree = ast.parse(path.read_text())
mods = set()
for node in tree.body:
if isinstance(node, ast.Import):
mods.update(a.name for a in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
mods.add(node.module)
return mods
class DependencyGraphTest(unittest.TestCase):
def test_leaf_modules_only_import_stdlib(self):
for name in ("orphans", "config", "compat215", "errors"):
mods = _top_level_imports(CHV / (name + ".py"))
for mod in mods:
top = mod.split(".")[0]
self.assertFalse(top.startswith("cloud"),
"%s 不得 import %s" % (name, mod))
def test_executor_never_imports_pool_or_orphans(self):
# 同上:只看 AST import 节点,模块头 docstring 的
# 分工说明不算 import。
tree = ast.parse((CHV / "executor.py").read_text())
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for a in node.names:
self.assertNotIn(a.name, (
"cloud.chv.pool",
"cloud.chv.experimental_pool",
"cloud.chv.orphans"))
if isinstance(node, ast.ImportFrom):
self.assertNotIn(node.module, (
"cloud.chv.pool",
"cloud.chv.experimental_pool",
"cloud.chv.orphans"))
def test_no_chv_function_over_50_lines(self):
over = []
for f in CHV.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, [])
class PublicApiTypesTest(unittest.TestCase):
def test_public_api_fully_annotated_and_no_any(self):
import re
unannotated = []
for f in CHV.glob("*.py"):
text = f.read_text()
for line in text.splitlines():
self.assertIsNone(re.search(r"\bAny\b", line),
"%s 含 Any 逃逸:%s" % (f.name, line))
tree = ast.parse(text)
for node in tree.body:
if isinstance(node, ast.FunctionDef) \
and not node.name.startswith("_"):
bad = [a.arg for a in node.args.args
if a.annotation is None]
if bad or node.returns is None:
unannotated.append("%s:%s" % (f.name, node.name))
if isinstance(node, ast.ClassDef):
for m in node.body:
if isinstance(m, ast.FunctionDef) and (
not m.name.startswith("_")
or m.name == "__init__"):
bad = [a.arg for a in m.args.args
if a.arg != "self"
and a.annotation is None]
bad += [a.arg for a in m.args.kwonlyargs
if a.annotation is None]
if bad or m.returns is None:
unannotated.append(
"%s:%s.%s" % (f.name, node.name,
m.name))
self.assertEqual(unannotated, [])
def test_corrected_annotations_match_runtime(self):
from cloud.chv.disk import record_snapshot
from cloud.chv.hostkeys import host_key_paths
with tempfile.TemporaryDirectory() as d:
entries, removed = record_snapshot(
d, "vm1", "https://example.invalid/snap", 20.0)
self.assertIsInstance(entries, list)
self.assertIsInstance(removed, list)
paths = host_key_paths(d, "vm1")
self.assertIsInstance(paths, dict)
self.assertIn("private", paths)
class ReplacementChecklistTest(unittest.TestCase):
def test_points_resolve_and_215_modules_absent(self):
by_id = {p["id"]: p for p in REPLACEMENT_POINTS}
self.assertEqual(set(by_id), {"R1", "R2", "R3"})
self.assertIsNotNone(resolve_inside)
self.assertIsNotNone(tmpfs_isolated)
hardening = (ROOT / "cloud" / "chv" / "hardening.py").read_text()
self.assertIn("resolve_inside", hardening)
for mod in ("cloud.gates", "cloud.sandbox_policy",
"cloud.timeouts", "cloud.quota"):
self.assertIsNone(importlib.util.find_spec(mod), mod)
import cloud.chv.compat215 as m
self.assertFalse(hasattr(m, "resolve_module_timeout"))
self.assertFalse(hasattr(m, "decide"))
self.assertIsNone(by_id["R3"]["replacement"])
self.assertIsNone(by_id["R2"]["caller"])
def test_tmpfs_isolated_has_no_production_caller(self):
hits = []
for f in (ROOT / "cloud").rglob("*.py"):
if "tests" in f.parts:
continue
if "tmpfs_isolated" in f.read_text():
hits.append(str(f.relative_to(ROOT)))
self.assertEqual(hits, ["cloud/chv/compat215.py"])
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 ServerLifecycleTest(unittest.TestCase):
def test_goal_fallback_form_also_derives_dead(self):
row = _row(executor="cloud-hypervisor",
interrupted_reason="VM 中途死亡")
self.assertTrue(_chv_vm_dead_from_row(row))
self.assertTrue(_task_to_dict(row).get("chv_vm_dead"))
def test_non_vm_reasons_never_derive(self):
for reason in ("quota", "worker_shutdown", None, ""):
row = _row(executor="cloud-hypervisor",
interrupted_reason=reason)
self.assertIsNone(_chv_vm_dead_from_row(row))
self.assertNotIn("chv_vm_dead", _task_to_dict(row))
row = _row(executor="docker",
interrupted_reason="VM 中途死亡,任务未完成")
self.assertIsNone(_chv_vm_dead_from_row(row))
if __name__ == "__main__":
unittest.main()