Files
codex-app-server/cloud/tests/test_chv_arch95.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

259 lines
10 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.
"""CHV 架构 9.5 提升轨测试(feat-chv-arch95,堆叠于 #246)。
只用标准库与 mock,绝不启动真实 VM。本文件是“严格测试替身”
的载体:替身不再无条件放行——fake API 校验 VmConfig 内容
(任务盘 + seed 盘、net 条目),fake SSH 校验 known_hosts
钉死与 StrictHostKeyChecking=yes,mock 再也不能把被测逻辑
本身也 mock 掉。测试里只用 FAKE 占位密钥,绝无真实密钥。
"""
import os
import sys
import tempfile
import unittest
from pathlib import Path
try:
from cloud.chv.experimental_pool import WarmPool
from cloud.sandbox import (
ChvFailure,
CloudHypervisorExecutor,
DockerExecutor,
parse_goal_result,
)
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.chv.experimental_pool import WarmPool
from cloud.sandbox import (
ChvFailure,
CloudHypervisorExecutor,
DockerExecutor,
parse_goal_result,
)
class FakeResult:
def __init__(self, returncode=0, stdout="", stderr=""):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
class FakeProc:
def __init__(self, alive=True):
self._alive = alive
def poll(self):
return None if self._alive else 1
def terminate(self):
self._alive = False
def wait(self, timeout=None):
return 0
def kill(self):
self._alive = False
def _write(path, content=b"x"):
with open(path, "wb") as fh:
fh.write(content)
os.chmod(path, 0o755)
return path
class Arch95Case(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = self.tmp.name
self.binary = _write(os.path.join(self.dir, "cloud-hypervisor"))
self.ch_remote = _write(os.path.join(self.dir, "ch-remote"))
self.kernel = _write(os.path.join(self.dir, "vmlinux"))
self.rootfs = _write(os.path.join(self.dir, "disk.raw"), b"R" * 100)
self.ssh_key = _write(os.path.join(self.dir, "id_chv"))
self.kvm = _write(os.path.join(self.dir, "kvm"))
self.cpuinfo = os.path.join(self.dir, "cpuinfo")
with open(self.cpuinfo, "w") as fh:
fh.write("flags : fpu vmx sse\n")
self.state_dir = os.path.join(self.dir, "state")
self.calls = []
self.audit = []
self.api_payloads = []
def tearDown(self):
self.tmp.cleanup()
def _runner(self, cmd, timeout=None):
self.calls.append(list(cmd))
if cmd and cmd[0] == "ssh-keygen":
priv = cmd[cmd.index("-f") + 1]
with open(priv, "w") as fh:
fh.write("-----BEGIN OPENSSH PRIVATE KEY-----\nFAKE\n")
os.chmod(priv, 0o600)
with open(priv + ".pub", "w") as fh:
fh.write("ssh-ed25519 AAAAfake1 vm-key\n")
return FakeResult(0, "", "")
if cmd and cmd[0] == "genisoimage":
out = cmd[cmd.index("-output") + 1]
with open(out, "wb") as fh:
fh.write(b"CIDATA" * 16)
return FakeResult(0, "", "")
return FakeResult(0, "", "")
def _strict_api(self, socket_path, method, path, payload=None):
"""严格替身:vm.create 必须带任务盘 + seed 盘、net 无重复。"""
if path == "/vm.create":
self.api_payloads.append(payload)
disks = [d.get("path") for d in payload.get("disks", [])]
self.assertEqual(len(disks), 2, "VmConfig 必须恰好两盘")
self.assertTrue(disks[0].endswith("disk.raw"),
"第一盘必须是任务盘")
self.assertTrue(disks[1].endswith("seed.iso"),
"第二盘必须是 seed 盘,缺 seed 即假保证")
self.assertTrue(payload.get("kernel", {}).get("path"))
nets = payload.get("net", [])
taps = [n.get("tap") for n in nets]
self.assertEqual(len(taps), len(set(taps)), "net tap 重复")
return (200, {})
def make_exec(self, api=None, runner=None, env_extra=None):
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
"CHV_SSH_FORWARD": "1"}
if env_extra:
env.update(env_extra)
ex = CloudHypervisorExecutor(
binary=self.binary, ch_remote=self.ch_remote,
kernel=self.kernel, rootfs=self.rootfs,
ssh_key=self.ssh_key, kvm_path=self.kvm,
cpuinfo_path=self.cpuinfo, state_dir=self.state_dir,
runner=runner or self._runner,
launcher=lambda cmd: FakeProc(alive=True),
api_client=api or self._strict_api,
sleeper=lambda s: None, env=env)
ex.audit_callback = lambda e, t, d, k=None: self.audit.append(
(e, t.get("id") if isinstance(t, dict) else t, d, k))
return ex
class WarmPoolDecisionTest(Arch95Case):
def test_pool_is_experimental_and_dispatch_is_cold_only(self):
self.assertFalse(WarmPool.wired_to_dispatch)
ex = self.make_exec()
pool = WarmPool(ex)
# 手工放一个 warm 条目:dispatch 必须不消耗它(冷启动路径)。
pool._entries["warm-1"] = {"state": "warm",
"task": {"id": "warm-1"},
"vm": "chv-warm-1",
"snapshot_url": None}
ex.wake_vm({"id": "t1"})
self.assertEqual(pool._entries["warm-1"]["state"], "warm")
self.assertEqual(pool.warm_count(), 1)
class StrictVmConfigTest(Arch95Case):
def test_wake_vm_config_has_task_disk_and_seed_disk(self):
ex = self.make_exec()
out = ex.wake_vm({"id": "t1"})
self.assertEqual(out["state"], "running")
self.assertEqual(len(self.api_payloads), 1)
# known_hosts 钉死:文件存在且含预置公钥,SSH 命令带钉死选项。
kh = ex.known_hosts_path({"id": "t1"})
with open(kh) as fh:
content = fh.read()
self.assertIn("AAAAfake1", content)
cmd = ex.build_ssh_command({"id": "t1"}, ["true"])
self.assertIn("StrictHostKeyChecking=yes", " ".join(cmd))
self.assertIn(kh, " ".join(cmd))
def test_strict_ssh_runner_rejects_missing_known_hosts(self):
def strict_runner(cmd, timeout=None):
if cmd and cmd[0] == "ssh":
self.assertIn("StrictHostKeyChecking=yes", cmd)
found = False
for i, a in enumerate(cmd):
if a.startswith("UserKnownHostsFile="):
path = a.split("=", 1)[1]
found = True
elif a == "UserKnownHostsFile":
path = cmd[i + 1]
found = True
else:
continue
with open(path) as fh:
self.assertTrue(fh.read().strip(),
"known_hosts 为空即假钉死")
self.assertTrue(found, "SSH 命令缺 known_hosts 钉死选项")
return self._runner(cmd, timeout=timeout)
ex = self.make_exec(runner=strict_runner)
ex.wake_vm({"id": "t1"})
# 严格 runner 在 _run_guest 里再跑一次 status 风格调用:
# 这里直接验证 build 出的命令能过严格 runner 的检查。
cmd = ex.build_ssh_command({"id": "t1"}, ["true"])
strict_runner(cmd)
class ConfigUnifiedTest(Arch95Case):
def test_preflight_unified_config_invalid(self):
cases = [
("ssh_port", 0), ("ssh_port", 70000),
("max_vms", -1), ("disk_gb", 0),
("api_timeout", 0), ("ready_timeout", -1),
("ready_interval", 0), ("warm_pool_size", -1),
("snapshot_max_count", -1),
("snapshot_max_total_gb", -1),
("snapshot_retention_days", -1),
("host_key_mode", "bogus"),
]
for field, bad in cases:
with self.subTest(field=field, bad=bad):
ex = self.make_exec()
setattr(ex, field, bad)
with self.assertRaises(ChvFailure) as ctx:
ex.preflight()
self.assertEqual(ctx.exception.kind, "config_invalid")
class DockerSyncGuardTest(unittest.TestCase):
"""Docker goal 内联版与正典 parse_goal_result 的同步守卫:
7 个真实标记用例 + 已存在撞名用例,两者状态必须全等;
任一实现改了分叉,测试即 fail(不再只靠文档清单)。"""
def _docker_status(self, stdout, returncode, stderr=""):
old = os.environ.get("XCM_API_KEY")
os.environ["XCM_API_KEY"] = "FAKE-TEST-KEY"
try:
ex = DockerExecutor(runner=lambda cmd, timeout=None:
FakeResult(returncode, stdout, stderr))
ex._call_runner = lambda cmd, timeout=None, env=None: \
FakeResult(returncode, stdout, stderr)
return ex.goal_task({"id": "t1", "goal": "do it",
"goal_max_rounds": 3})["status"]
finally:
if old is None:
del os.environ["XCM_API_KEY"]
else:
os.environ["XCM_API_KEY"] = old
def test_docker_and_canonical_goal_status_equal(self):
cases = [
("goal met after rounds\n", 0, ""),
("goal unmet after rounds\nround 1\nverify_out=fail\n", 0, ""),
("goal interrupted after rounds: boom\n", 1, ""),
("", 1, "boom"),
("KeyError: breakpoint\n", 1, ""),
("", 0, ""),
("goal-resume 拒绝:breakpoint lost\n", 1, ""),
("任务已存在\n", 1, ""),
]
for stdout, rc, stderr in cases:
with self.subTest(stdout=stdout[:30], rc=rc):
canon = parse_goal_result(
"t1", FakeResult(rc, stdout, stderr), "local")
docker = self._docker_status(stdout, rc, stderr)
self.assertEqual(docker, canon["status"])
if __name__ == "__main__":
unittest.main()