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

647 lines
26 KiB
Python
Raw Permalink 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 审核修复轨测试(堆叠于 #230,base=feat-chv-gaps)。
只用标准库与 mock runner/launcher/api_client,绝不启动真实
VM:当前环境没有可跑真 VM 的宿主。真实 VM 启动、seed 盘
在真机上的 cloud-init 应用、SSH 可达实测一律不在本文件
声称已验证;本文件只证明代码层面的接线(VmConfig 含 seed
盘、restore 不回落、reuse 拦截、SSH 目标解析、配额/快照/
API 完整性等)成立。
"""
import json
import os
import socket
import sys
import tempfile
import threading
import unittest
from unittest import mock
from pathlib import Path
from types import SimpleNamespace
try:
from cloud.chv.api_client import _unix_socket_api
from cloud.chv.disk import check_disk_quota, provision_task_disk
from cloud.chv.hostkeys import build_seed_disk, seed_disk_path
from cloud.chv.experimental_pool import WarmPool
from cloud.sandbox import (
ChvFailure,
CloudHypervisorExecutor,
)
from cloud.worker import Worker
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.chv.api_client import _unix_socket_api
from cloud.chv.disk import check_disk_quota, provision_task_disk
from cloud.chv.hostkeys import build_seed_disk, seed_disk_path
from cloud.chv.experimental_pool import WarmPool
from cloud.sandbox import (
ChvFailure,
CloudHypervisorExecutor,
)
from cloud.worker import Worker
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 ChvFixCase(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.api_calls = []
self.api_payloads = {}
self.audit = []
def tearDown(self):
self.tmp.cleanup()
def _default_runner(self, cmd, timeout=None):
self.calls.append(list(cmd))
if cmd and cmd[0] == "ssh":
if cmd[-1] == "true":
return FakeResult(0, "", "")
if "goal" in cmd:
return FakeResult(0, "goal met after rounds\n", "")
return FakeResult(0, "", "")
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 _default_api(self, socket_path, method, path, payload=None):
self.api_calls.append((method, path))
if path == "/vm.create":
self.api_payloads["vm.create"] = payload
return (200, {})
def make_exec(self, runner=None, env_extra=None, **overrides):
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
"CHV_SSH_FORWARD": "1"}
if env_extra:
env.update(env_extra)
kwargs = dict(
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._default_runner,
launcher=lambda cmd: FakeProc(alive=True),
api_client=self._default_api,
sleeper=lambda s: None, env=env)
kwargs.update(overrides)
ex = CloudHypervisorExecutor(**kwargs)
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
# -- P0-1 seed 磁盘接入 -------------------------------------------------
class SeedDiskTest(ChvFixCase):
def test_vm_config_carries_seed_disk_readonly(self):
ex = self.make_exec()
# feat-chv-secfix(L2):磁盘路径须在 state_dir 内,
# 旧 /tmp/x 路径不再合法,改用 state_dir 下路径。
disk = os.path.join(self.state_dir, "x", "disk.raw")
seed = os.path.join(self.state_dir, "x", "seed.iso")
cfg = ex.build_vm_config(disk, seed_disk=seed)
self.assertEqual(len(cfg["disks"]), 2)
self.assertEqual(cfg["disks"][1]["path"], seed)
self.assertTrue(cfg["disks"][1]["readonly"])
cfg2 = ex.build_vm_config(disk)
self.assertEqual(len(cfg2["disks"]), 1)
def test_vm_config_rejects_disk_outside_state_dir(self):
ex = self.make_exec()
with self.assertRaises(ChvFailure) as ctx:
ex.build_vm_config("/tmp/x/disk.raw")
self.assertEqual(ctx.exception.kind, "disk_path_not_allowed")
def test_provisioned_wake_mounts_seed_disk(self):
ex = self.make_exec(
env_extra={"CHV_HOST_KEY_MODE": "provisioned"})
out = ex.wake_vm({"id": "seed1"})
self.assertEqual(out["state"], "running")
payload = self.api_payloads["vm.create"]
self.assertEqual(len(payload["disks"]), 2)
seed_disk = payload["disks"][1]["path"]
self.assertTrue(seed_disk.endswith("seed.iso"))
self.assertTrue(payload["disks"][1]["readonly"])
self.assertTrue(os.path.exists(seed_disk))
self.assertIn("genisoimage", [c[0] for c in self.calls])
def test_seed_disk_build_failure_is_fail_fast(self):
def bad_runner(cmd, timeout=None):
self.calls.append(list(cmd))
if cmd and cmd[0] == "genisoimage":
return FakeResult(1, "", "no genisoimage")
return self._default_runner(cmd, timeout)
ex = self.make_exec(
runner=bad_runner,
env_extra={"CHV_HOST_KEY_MODE": "provisioned"})
with self.assertRaises(RuntimeError):
ex.wake_vm({"id": "seedfail"})
# 制盘失败在起 VMM 之前,不应走到 vm.create。
self.assertNotIn("/vm.create",
[p for _, p in self.api_calls])
def test_build_seed_disk_rejects_missing_product(self):
with self.assertRaises(RuntimeError):
build_seed_disk(self.dir, os.path.join(self.dir, "no.iso"),
runner=lambda cmd: FakeResult(0, "", ""))
# -- P0-2 restore 不回落 ------------------------------------------------
class RestoreNoFallbackTest(ChvFixCase):
def test_wait_guest_ready_provisioned_missing_known_hosts(self):
ex = self.make_exec(
env_extra={"CHV_HOST_KEY_MODE": "provisioned"})
with self.assertRaises(ChvFailure) as ctx:
ex.wait_guest_ready({"id": "nopin"})
self.assertEqual(ctx.exception.kind, "host_key_not_pinned")
self.assertFalse(any(c[0] == "ssh-keyscan" for c in self.calls))
def test_restore_provisioned_missing_known_hosts_fails_before_launch(self):
ex = self.make_exec(
env_extra={"CHV_HOST_KEY_MODE": "provisioned"})
launches = []
ex.launcher = lambda cmd: launches.append(cmd) or FakeProc(True)
# feat-chv-secfix(L2):恢复源须在白名单目录内,
# 本测试的意图是“起 VMM 前失败”,用 state_dir 下路径。
snap_url = "file://" + os.path.join(self.state_dir, "snap", "x")
with self.assertRaises(ChvFailure) as ctx:
ex.restore_vm({"id": "r1"}, snap_url)
self.assertEqual(ctx.exception.kind, "host_key_not_pinned")
self.assertEqual(launches, [])
# -- P0-3 reuse 拦截 ----------------------------------------------------
class ReuseBlockTest(ChvFixCase):
def test_reuse_blocked_without_experiment_flag(self):
ex = self.make_exec(env_extra={"CHV_DISK_MODE": "reuse"})
with self.assertRaises(ChvFailure) as ctx:
ex.preflight()
self.assertEqual(ctx.exception.kind,
"disk_mode_reuse_blocked")
def test_reuse_passes_with_experiment_flag_and_is_audited(self):
ex = self.make_exec(env_extra={
"CHV_DISK_MODE": "reuse",
"CHV_REUSE_SINGLE_TENANT_EXPERIMENT": "1",
# feat-chv-secfix(M6):实验放行还须声明单租户项目。
"CHV_SINGLE_TENANT_PROJECT": "p1"})
self.assertTrue(ex.preflight())
out = ex.wake_vm({"id": "reuse1"})
self.assertEqual(out["state"], "running")
self.assertTrue(any(e == "disk_reuse_experiment_allowed"
for e, _, _, _ in self.audit))
# -- P0-4 SSH 目标解析 --------------------------------------------------
class SshTargetTest(ChvFixCase):
def test_explicit_host_wins(self):
ex = self.make_exec(ssh_host="10.9.8.7")
self.assertEqual(ex.resolve_ssh_host(), "10.9.8.7")
def test_net_guest_ip_used_when_no_explicit_host(self):
ex = self.make_exec(env_extra={
"CHV_NET_JSON": '[{"tap": "", "ip": "192.168.100.5",'
'"mask": "255.255.255.0"}]',
"CHV_SSH_FORWARD": "0"})
# env_extra 覆盖 forward=0 后仍应走 net ip,不报未声明。
self.assertEqual(ex.resolve_ssh_host(), "192.168.100.5")
cmd = ex.build_ssh_command({"id": "t"}, ["true"])
self.assertIn("root@192.168.100.5", " ".join(cmd))
def test_undeclared_target_is_fail_fast(self):
ex = self.make_exec(env_extra={"CHV_SSH_FORWARD": "0"})
with self.assertRaises(ChvFailure) as ctx:
ex.resolve_ssh_host()
self.assertEqual(ctx.exception.kind, "ssh_target_undeclared")
def test_forward_attestation_allows_loopback(self):
ex = self.make_exec()
self.assertEqual(ex.resolve_ssh_host(), "127.0.0.1")
# -- P1-1 网关 extra_env ------------------------------------------------
class GatewayExtraEnvTest(ChvFixCase):
def test_chv_ssh_command_carries_extra_env(self):
# feat-chv-secfix(H2):密钥值绝不再进 argv。
# 非密钥 extra_env 仍走 env 前缀;密钥子集须 seed
# 投递后才 source 客内文件(见 worker 网关测试)。
ex = self.make_exec()
cmd = ex.build_ssh_command({"id": "t"}, ["true"],
extra_env={"SUB2API_BASE_URL":
"http://gw/"})
joined = " ".join(cmd)
self.assertIn("SUB2API_BASE_URL=http://gw/", joined)
self.assertIn("HTTPS_PROXY=http://10.0.0.1:8888", joined)
with self.assertRaises(ChvFailure):
ex.build_ssh_command({"id": "t"}, ["true"],
extra_env={"SUB2API_API_KEY": "k1"})
def test_worker_gateway_guard_allows_chv(self):
from cloud.server import init_db
db_path = os.path.join(self.dir, "control.db")
init_db(db_path)
ex = self.make_exec()
worker = Worker(db_path, "/fake/host.py", executor=ex,
env={"XCM_API_KEY": "test-placeholder"})
task = {"id": "gw1", "project_id": "p1", "prompt": "做目标",
"goal_mode": 1,
"_gateway_provider": "sub2api",
"_gateway_env": {"SUB2API_API_KEY": "gwkey",
"SUB2API_BASE_URL": "http://gw/"}}
result, error = worker._run_attempt(task)
self.assertIsNone(error)
self.assertEqual(result["status"], "completed")
ssh_calls = [c for c in self.calls if c and c[0] == "ssh"]
# feat-chv-secfix(H2):密钥值不得出现在任何 ssh
# argv 里;远程命令改为 source 客内 seed 密钥文件。
self.assertFalse(any("SUB2API_API_KEY=gwkey" in " ".join(c)
for c in ssh_calls))
self.assertTrue(any("/etc/chv/guest-env" in " ".join(c)
for c in ssh_calls))
# 任务结束 stop 后 seed 目录与宿主私钥均已清(H3/H5;
# seed 内容本身的断言见 test_chv_secfix 的直接
# provision_host_key 测试,stop 前 seed 仍在)。
self.assertFalse(os.path.exists(
os.path.join(self.state_dir, "chv-gw1", "seed")))
self.assertFalse(os.path.exists(
os.path.join(self.state_dir, "chv-gw1",
"ssh_host_ed25519_key")))
# -- P1-2 取消/清理/看门狗 ----------------------------------------------
class CancelCleanupTest(ChvFixCase):
def test_worker_cancel_calls_cancel_vm(self):
from cloud.server import init_db
db_path = os.path.join(self.dir, "control.db")
init_db(db_path)
ex = self.make_exec()
cancelled = []
ex.cancel_vm = lambda name: cancelled.append(name) or True
worker = Worker(db_path, "/fake/host.py", executor=ex,
env={"XCM_API_KEY": "test-placeholder"})
worker._running_set("t-cancel")
import sqlite3, time
conn = sqlite3.connect(db_path)
conn.execute(
"INSERT INTO tasks(id, project_id, prompt, status,"
" idempotency_key, depends_on, priority, created_by,"
" created_at, updated_at, last_heartbeat)"
" VALUES(?, ?, ?, ?, NULL, ?, ?, 'tester', ?, ?, NULL)",
("t-cancel", "p1", "做事", "running", "[]", 0,
time.time(), time.time()))
conn.commit()
conn.close()
self.assertTrue(worker.cancel("t-cancel"))
self.assertEqual(cancelled, ["t-cancel"])
def test_worker_cleanup_calls_cleanup_stale_vms(self):
from cloud.server import init_db
db_path = os.path.join(self.dir, "control.db")
init_db(db_path)
ex = self.make_exec()
ex.cleanup_stale_vms = lambda: ["chv-stale"]
worker = Worker(db_path, "/fake/host.py", executor=ex,
env={"XCM_API_KEY": "test-placeholder"})
self.assertIn("chv-stale", worker.cleanup_stale_containers())
def test_chv_cancel_watchdog_kills_vm_on_cancel(self):
import threading
from cloud.server import init_db
db_path = os.path.join(self.dir, "control.db")
init_db(db_path)
ex = self.make_exec()
killed = []
ex.cancel_vm = lambda name: killed.append(name) or True
worker = Worker(db_path, "/fake/host.py", executor=ex,
env={"XCM_API_KEY": "test-placeholder"})
worker._cancel_requested.add("t-wd")
stop = threading.Event()
worker._chv_cancel_watchdog("t-wd", "chv-t-wd", stop)
self.assertEqual(killed, ["chv-t-wd"])
# -- P1-3 WarmPool acquire resume ---------------------------------------
class PoolAcquireResumeTest(unittest.TestCase):
def test_acquire_resumes_paused_vm(self):
class Stub:
def __init__(self):
self.resumed = []
self.state_dir = None
self.warm_pool_size = 1
self.snapshot_url = "file:///snap/base"
def vm_name(self, task):
raw = task.get("id") if isinstance(task, dict) \
else str(task)
return raw if raw.startswith("chv-") \
else "chv-" + raw
def wake_vm(self, task):
return {"vm": self.vm_name(task)}
def pause_vm(self, task):
return True
def resume_vm(self, task):
self.resumed.append(task["id"])
return True
def shutdown_vm(self, task_or_name):
return True
def _emit(self, event, task, detail="", kind=None):
pass
stub = Stub()
pool = WarmPool(stub, size=1, snapshot_url="file:///snap/base",
registry_path=None)
pool.prewarm()
got = pool.acquire({"id": "t1"})
self.assertEqual(got["mode"], "warm")
self.assertEqual(stub.resumed, ["warm-1"])
def test_acquire_discards_vm_whose_resume_fails(self):
class Stub:
def __init__(self):
self.state_dir = None
self.warm_pool_size = 1
self.snapshot_url = "file:///snap/base"
def vm_name(self, task):
raw = task.get("id") if isinstance(task, dict) \
else str(task)
return raw if raw.startswith("chv-") \
else "chv-" + raw
def wake_vm(self, task):
return {"vm": self.vm_name(task)}
def pause_vm(self, task):
return True
def resume_vm(self, task):
raise RuntimeError("resume 失败")
def shutdown_vm(self, task_or_name):
return True
def _emit(self, event, task, detail="", kind=None):
pass
pool = WarmPool(Stub(), size=1, snapshot_url="file:///snap/base",
registry_path=None)
pool.prewarm()
self.assertEqual(pool.acquire({"id": "t1"})["mode"], "cold")
# -- P1-4 容量持久化计数 -------------------------------------------------
class CapacityPersistentTest(ChvFixCase):
def test_socket_counts_after_restart(self):
# feat-chv-secfix(M7):只有真能连通的 socket 才
# 计数——用真实监听 socket 代表活 VMM。
ex = self.make_exec(env_extra={"CHV_MAX_VMS": "1"})
os.makedirs(self.state_dir, exist_ok=True)
sock_path = os.path.join(self.state_dir, "chv-other.sock")
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
srv.bind(sock_path)
srv.listen(1)
try:
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm({"id": "new1"})
self.assertEqual(ctx.exception.kind, "host_capacity_exceeded")
finally:
srv.close()
def test_dead_socket_file_does_not_block(self):
# M7 的另一面:空文件/死 socket 不再占满容量。
ex = self.make_exec(env_extra={"CHV_MAX_VMS": "1"})
os.makedirs(self.state_dir, exist_ok=True)
with open(os.path.join(self.state_dir, "chv-other.sock"),
"w") as fh:
fh.write("")
out = ex.wake_vm({"id": "new1"})
self.assertEqual(out["state"], "running")
def test_own_socket_does_not_block_rewake(self):
ex = self.make_exec(env_extra={"CHV_MAX_VMS": "1"})
os.makedirs(self.state_dir, exist_ok=True)
with open(os.path.join(self.state_dir, "chv-t1.sock"),
"w") as fh:
fh.write("")
# 同任务重复 wake:自己的残留 socket 不计入容量。
out = ex.wake_vm({"id": "t1"})
self.assertEqual(out["state"], "running")
# -- P1-5 磁盘配额 -------------------------------------------------------
class DiskQuotaTest(ChvFixCase):
def test_unreadable_image_is_not_silently_skipped(self):
with self.assertRaises(ChvFailure) as ctx:
check_disk_quota("/nonexistent/disk.raw", 1.0)
self.assertEqual(ctx.exception.kind, "disk_quota_unverified")
def test_total_usage_over_quota(self):
os.makedirs(self.state_dir, exist_ok=True)
with open(os.path.join(self.state_dir, "old.raw"),
"wb") as fh:
fh.write(b"X" * 100)
# 配额 150 字节:已用 100 + 本次 100 > 150。
with self.assertRaises(ChvFailure) as ctx:
check_disk_quota(self.rootfs, 150 / (1024 ** 3),
state_dir=self.state_dir)
self.assertEqual(ctx.exception.kind, "disk_quota_exceeded")
def test_existing_disk_size_mismatch_is_recopied(self):
task_disk = provision_task_disk(self.state_dir, "chv-a",
self.rootfs, "copy")
with open(task_disk, "wb") as fh:
fh.write(b"Z" * 10)
task_disk2 = provision_task_disk(self.state_dir, "chv-a",
self.rootfs, "copy")
self.assertEqual(task_disk, task_disk2)
with open(task_disk2, "rb") as fh:
self.assertEqual(fh.read(), b"R" * 100)
# -- P1-6 API 截断 -------------------------------------------------------
class ApiTruncationTest(unittest.TestCase):
def _serve(self, raw):
tmp = tempfile.TemporaryDirectory()
sock_path = os.path.join(tmp.name, "vmm.sock")
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
srv.bind(sock_path)
srv.listen(1)
def _run():
conn, _ = srv.accept()
try:
conn.recv(65536)
conn.sendall(raw)
finally:
conn.close()
srv.close()
threading.Thread(target=_run, daemon=True).start()
return tmp, sock_path
def test_truncated_body_raises(self):
body = b'{"ok": true}'
raw = (b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n"
+ body)
tmp, sock_path = self._serve(raw)
with self.assertRaises(RuntimeError):
_unix_socket_api(sock_path, "GET", "/vmm.ping")
tmp.cleanup()
def test_chunked_rejected(self):
raw = (b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
b"5\r\nhello\r\n0\r\n\r\n")
tmp, sock_path = self._serve(raw)
with self.assertRaises(RuntimeError):
_unix_socket_api(sock_path, "GET", "/vmm.ping")
tmp.cleanup()
def test_full_body_parses(self):
body = b'{"ok": true}'
raw = (b"HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n"
% len(body)) + body
tmp, sock_path = self._serve(raw)
status, parsed = _unix_socket_api(sock_path, "GET", "/vmm.ping")
self.assertEqual(status, 200)
self.assertTrue(parsed["ok"])
tmp.cleanup()
# -- P1-7 快照落盘验证 ---------------------------------------------------
class SnapshotPersistTest(ChvFixCase):
def test_snapshot_missing_file_does_not_shutdown(self):
ex = self.make_exec()
# feat-chv-secfix(L2):快照须在白名单目录内,
# 用 state_dir 下的不存在路径,仍验 snapshot_not_persisted。
missing = os.path.join(self.state_dir, "missing.snap")
with self.assertRaises(ChvFailure) as ctx:
ex.hibernate_vm({"id": "h1"}, "file://" + missing)
self.assertEqual(ctx.exception.kind, "snapshot_not_persisted")
kinds = [c[-1] for c in self.calls if c[0] == self.ch_remote]
self.assertIn("pause", kinds)
self.assertNotIn("shutdown", kinds)
# -- P1-8 execute 死端口归类 ---------------------------------------------
class ExecuteDeadPortTest(ChvFixCase):
def test_dead_socket_ssh_failure_is_vm_not_running(self):
def raising_runner(cmd, timeout=None):
self.calls.append(list(cmd))
if cmd and cmd[0] == "ssh":
raise RuntimeError("connection refused")
return FakeResult(0, "", "")
ex = self.make_exec(runner=raising_runner)
os.makedirs(self.state_dir, exist_ok=True)
with open(os.path.join(self.state_dir, "chv-dead.sock"),
"w") as fh:
fh.write("")
ex._vms["chv-dead"] = FakeProc(alive=False)
with self.assertRaises(ChvFailure) as ctx:
ex.execute({"id": "dead"})
self.assertEqual(ctx.exception.kind, "vm_not_running")
# -- P2 配置校验时机 / 文档口径 ------------------------------------------
class ConfigTimingAndDocsTest(ChvFixCase):
def test_preflight_rejects_bad_cpu_and_memory(self):
ex = self.make_exec(cpus="abc")
with self.assertRaises(ChvFailure) as ctx:
ex.preflight()
self.assertEqual(ctx.exception.kind, "config_invalid")
ex2 = self.make_exec(memory="not-a-size")
with self.assertRaises(ChvFailure) as ctx2:
ex2.preflight()
self.assertEqual(ctx2.exception.kind, "config_invalid")
def test_stop_cleanup_failure_is_audited(self):
ex = self.make_exec()
ex.stop_vm({"id": "ck"})
os.makedirs(os.path.join(self.state_dir, "chv-ck", "seed"),
exist_ok=True)
with mock.patch("cloud.chv.executor.shutil.rmtree",
side_effect=OSError("rm failed")):
ex.stop_vm({"id": "ck"})
self.assertTrue(any(e == "vm_key_cleanup_failed"
for e, _, _, _ in self.audit))
def test_readme_sections_in_order_and_buildinfo_honest(self):
readme = Path(__file__).resolve().parents[2] \
/ "chv-guest" / "README.md"
text = readme.read_text()
self.assertLess(text.index("## 5. Host key"),
text.index("## 6. 与执行器的对接"))
self.assertIn("contents: [清单逐项名]", text)
self.assertIn("Node.js 不装", text)
def test_manual_vm_command_is_private(self):
ex = self.make_exec()
self.assertFalse(hasattr(ex, "build_vm_command"))
cmd = ex._build_vm_command({"id": "t"})
self.assertIn("--api-socket", cmd)
if __name__ == "__main__":
unittest.main()