"""CHV 补缺口轨测试(堆叠于 #221,base=feat-chv-optimize)。 只用标准库与 mock runner/launcher/api_client,绝不启动真实 VM:当前环境没有可跑真 VM 的宿主(生产主机是 CVM、无 /dev/kvm、无嵌套虚拟化;本 VM 容器内同样无 /dev/kvm)。 真实 VM 启动、快照恢复、压测一律不在本文件声称已验证。 """ import json import os import subprocess import sys import tempfile import unittest from pathlib import Path from types import SimpleNamespace import yaml try: from cloud.chv.hostkeys import ( build_seed_user_data, generate_host_keypair, host_key_paths, known_hosts_line, write_seed, ) from cloud.chv.experimental_pool import WarmPool from cloud.sandbox import ( ChvFailure, CloudHypervisorExecutor, _parse_memory_bytes, ) from cloud.worker import Worker, _call_goal_resume_breakpoint except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.chv.hostkeys import ( build_seed_user_data, generate_host_keypair, host_key_paths, known_hosts_line, write_seed, ) from cloud.chv.experimental_pool import WarmPool from cloud.sandbox import ( ChvFailure, CloudHypervisorExecutor, _parse_memory_bytes, ) from cloud.worker import Worker, _call_goal_resume_breakpoint 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 def _public_resolver(host, port): """feat-chv-muse-p9:假解析器(公网 IP),供推理出口预审用; 实验室绝不联网,真实解析只在真机发生。""" return ["93.184.216.34"] class ChvGapCase(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 = [] 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-resume" in cmd: return FakeResult(0, "goal met after rounds\n", "") if "goal" in cmd: return FakeResult(0, "goal met after rounds\n", "") return FakeResult(0, "", "") return FakeResult(0, "", "") def _default_api(self, socket_path, method, path, payload=None): return (200, {}) def _default_launcher(self, cmd): return FakeProc(alive=True) def make_exec(self, runner=None, env_extra=None, **overrides): env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888", # 实验室宣称:mock runner 假装回环端口已转发到客机 #(feat-chv-fix 起,执行器不再无条件假设回环能通)。 # 实验室显式 opt-in keyscan:生产默认已是 # provisioned;本套件旧 fake 不实现 ssh-keygen/ # genisoimage,测的不是 host key 模式,故显式 # 声明实验室路径,绝不把它写成生产默认。 "CHV_HOST_KEY_MODE": "keyscan", "CHV_SSH_FORWARD": "1", # feat-chv-muse-p9:推理出口预审的假 origin(公网 # https,配假解析器;实验室不起真 VM、不联网)。 "SUB2API_BASE_URL": "https://xgapi.lol", "CHV_INFERENCE_ORIGIN": "https://xgapi.lol"} 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=self._default_launcher, api_client=self._default_api, sleeper=lambda s: None, env=env) kwargs.update(overrides) ex = CloudHypervisorExecutor(**kwargs) ex.sentinel_resolver = _public_resolver 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 # -- (1) worker 断点接线 --------------------------------------------- class BreakpointWiringTest(ChvGapCase): def test_call_helper_passes_model_provider_when_accepted(self): seen = {} class FakeChv: def goal_resume_breakpoint_task(self, task, model_provider=None): seen["provider"] = model_provider return {"status": "completed", "task_id": task["id"]} out = _call_goal_resume_breakpoint( FakeChv(), {"id": "bp1"}, model_provider="prov-x") self.assertEqual(out["status"], "completed") self.assertEqual(seen["provider"], "prov-x") def test_call_helper_single_arg_executor_still_works(self): class FakeOld: def goal_resume_breakpoint_task(self, task): return {"status": "completed", "task_id": task["id"]} out = _call_goal_resume_breakpoint( FakeOld(), {"id": "bp2"}, model_provider="prov-x") self.assertEqual(out["status"], "completed") def test_chv_breakpoint_accepts_model_provider(self): ex = self.make_exec() out = ex.goal_resume_breakpoint_task( {"id": "g1", "prompt": "续跑"}, model_provider="prov-x") self.assertEqual(out["status"], "completed") self.assertTrue(out["goal_met"]) # 客内命令带 --model-provider(网关 key 本身不进命令值)。 ssh_calls = [c for c in self.calls if c and c[0] == "ssh"] self.assertTrue(any("--model-provider" in c for c in ssh_calls)) def test_worker_breakpoint_branch_calls_generic_executor(self): # feat-chv-fix(P2-3):不再用源码 grep 充数,改为行为 # 断言——真实 Worker._run_attempt 走通用断点分支,调到 # 非 Local 执行器的 goal_resume_breakpoint_task,并把 # 结果原样返回(参数、守卫顺序、返回值都覆盖)。 from cloud.server import init_db db_path = os.path.join(self.dir, "control.db") init_db(db_path) seen = {} class FakeGeneric: def goal_resume_breakpoint_task(self, task, model_provider=None, extra_env=None): seen["provider"] = model_provider seen["task_id"] = task["id"] return {"executor": "generic", "task_id": task["id"], "status": "completed", "stdout": "", "stderr": "", "returncode": 0, "host_status": None, "goal_met": True, "goal_error": None} worker = Worker(db_path, "/fake/host.py", executor=FakeGeneric(), env={"XCM_API_KEY": "test-placeholder"}) task = {"id": "bp-behav", "project_id": "p1", "prompt": "续跑", "goal_mode": 1, "_goal_resume_breakpoint": True} result, error = worker._run_attempt(task) self.assertIsNone(error) self.assertEqual(result["status"], "completed") self.assertTrue(result["goal_met"]) self.assertEqual(seen["task_id"], "bp-behav") self.assertNotIn("executor 不支持", str(result)) # -- (2) reconcile_orphans 接线 --------------------------------------- class ReconcileWiringTest(ChvGapCase): def test_worker_startup_and_close_call_reconcile(self): from cloud.server import init_db db_path = os.path.join(self.dir, "control.db") init_db(db_path) ex = self.make_exec() # 手工造一个 orphan:先建池写 registry,再新开池载入。 pool1 = WarmPool(ex) pool1._entries["chv-old"] = { "state": "warm", "vm": "chv-old", "task": {"id": "old"}, "snapshot_url": None} pool1._save_registry() self.assertTrue( os.path.exists(os.path.join(self.state_dir, "warm-pool.json"))) # Worker 构造即 startup reconcile:用假 shutdown 记录。 # 本轨 worker 不再持有 _warm_pool(生产走 orphans # facade);实验池只用于本测试造 registry 种子。 shutdowns = [] orig_shutdown = ex.shutdown_vm ex.shutdown_vm = lambda name: shutdowns.append(name) or True worker = Worker(db_path, "/fake/host.py", executor=ex, env={"XCM_API_KEY": "test-placeholder"}) self.assertIn("chv-old", shutdowns) self.assertFalse(hasattr(worker, "_warm_pool")) with open(os.path.join(self.state_dir, "warm-pool.json")) as fh: self.assertEqual(json.load(fh), {}) worker.close() # close 时再次 reconcile 不炸(池已空,返回 [] 即可)。 self.assertEqual(worker._reconcile_warm_pool("shutdown"), []) def test_reconcile_failure_does_not_break_close(self): from cloud.server import init_db import cloud.chv.orphans as orphans 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"}) # facade 抛错时 close 仍不炸(worker 方法内每次现取 # facade,猴子补丁模块属性即生效)。 orig = orphans.reconcile_orphans_for_executor orphans.reconcile_orphans_for_executor = lambda ex: ( (_ for _ in ()).throw(RuntimeError("boom"))) try: worker.close() # 不抛即通过 finally: orphans.reconcile_orphans_for_executor = orig # -- (3) SSH host key 预置 --------------------------------------------- class HostKeyProvisionTest(ChvGapCase): def _fake_keygen_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, "", "") return self._default_runner(cmd, timeout) def test_per_vm_keys_differ_and_known_hosts_pinned(self): ex = self.make_exec(runner=self._fake_keygen_runner) r1 = ex.provision_host_key({"id": "vm-a"}) r2 = ex.provision_host_key({"id": "vm-b"}) # 不同 VM 的私钥文件不同路径、known_hosts 不同文件。 self.assertNotEqual(r1["paths"]["private"], r2["paths"]["private"]) self.assertNotEqual(r1["known_hosts"], r2["known_hosts"]) for r in (r1, r2): with open(r["known_hosts"]) as fh: line = fh.read().strip() self.assertTrue(line.startswith("[127.0.0.1]:")) self.assertIn("ssh-ed25519 AAAAfake1", line) self.assertNotIn("PRIVATE KEY", line) # 同一 VM 再次预置复用已有对(不重生成、不改 known_hosts)。 with open(r1["known_hosts"]) as fh: before = fh.read() ex.provision_host_key({"id": "vm-a"}) with open(r1["known_hosts"]) as fh: self.assertEqual(fh.read(), before) def test_seed_contains_private_key_and_is_deleted_on_stop(self): ex = self.make_exec(runner=self._fake_keygen_runner) r = ex.provision_host_key({"id": "vm-c"}) seed_user = os.path.join(r["seed_dir"], "user-data") self.assertTrue(os.path.exists(seed_user)) with open(seed_user) as fh: seed_text = fh.read() self.assertIn("ssh_host_ed25519_key", seed_text) self.assertIn("ssh_deletekeys: false", seed_text) # stop 时 seed 目录与私钥删掉,known_hosts 公钥保留审计。 ex.stop_vm({"id": "vm-c"}) self.assertFalse(os.path.exists(r["seed_dir"])) self.assertFalse(os.path.exists(r["paths"]["private"])) self.assertTrue(os.path.exists(r["known_hosts"])) def test_known_hosts_line_port_22_is_bare(self): # 22 端口写裸 host 行:OpenSSH 默认端口只查裸名, # '[host]:22' 永远匹配不上(2026-11-07 WSL 真机)。 self.assertEqual( known_hosts_line(22, "ssh-ed25519 AAAAx comment", host="192.168.10.10"), "192.168.10.10 ssh-ed25519 AAAAx") self.assertEqual( known_hosts_line(2222, "ssh-ed25519 AAAAx comment"), "[127.0.0.1]:2222 ssh-ed25519 AAAAx") def test_known_hosts_line_rejects_bad_key(self): with self.assertRaises(ValueError): known_hosts_line(2222, "not-a-key") self.assertEqual( known_hosts_line(2222, "ssh-ed25519 AAAAx comment"), "[127.0.0.1]:2222 ssh-ed25519 AAAAx") def test_seed_user_data_shape(self): text = build_seed_user_data("PRIV\n", "ssh-ed25519 AAAAx\n") self.assertIn("/etc/ssh/ssh_host_ed25519_key", text) self.assertIn("ssh_deletekeys: false", text) with self.assertRaises(ValueError): build_seed_user_data("", "ssh-ed25519 AAAAx") def test_seed_user_data_has_no_runcmd(self): # 根因回归(sync-test 残留,2026-11-06 实测):seed # 的 user-data 里绝不能带 runcmd——cloud-init 不跨配 # 置类型合并,user-data 的 runcmd 会整体替换镜像 # /etc/cloud/cloud.cfg.d/ 里的 runcmd,旧版 seed 的 # 两行就这样顶掉了 99-chv-sync.cfg 的 chv-sync boot # 钩子,首轮启动 clone 静默不跑。sshd 重启已搬到 # 镜像自己的 runcmd(build.sh 写 99-chv-sync.cfg)。 text = build_seed_user_data("PRIV\n", "ssh-ed25519 AAAAx\n") parsed = yaml.safe_load(text) self.assertNotIn("runcmd", parsed) self.assertNotIn("runcmd:", text) self.assertNotIn("systemctl restart", text) self.assertNotIn("merge_how", parsed) # write_files 两条 key 条目仍在,chmod 靠其 0600 保证。 self.assertEqual(len(parsed["write_files"]), 2) self.assertEqual( parsed["write_files"][0]["permissions"], "0600") def test_write_seed_with_secret_has_no_runcmd(self): # 带 secret_env 时 guest-env 条目仍须并进同一个 # write_files 列表(单 write_files 键),且 user-data # 里仍不能出现 runcmd。 with tempfile.TemporaryDirectory() as tmp: seed_dir = os.path.join(tmp, "seed") write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx", secret_env={"GITEA_TOKEN": "s3cr3t v"}) with open(os.path.join(seed_dir, "user-data")) as fh: content = fh.read() self.assertEqual(content.count("write_files:"), 1) parsed = yaml.safe_load(content) self.assertNotIn("runcmd", parsed) self.assertEqual(len(parsed["write_files"]), 3) paths = [e["path"] for e in parsed["write_files"]] self.assertIn("/etc/chv/guest-env", paths) # 密钥值经 shlex.quote 引用后才进文件(值本身在 guest-env # 条目内是设计内的,见 hostkeys.write_seed)。 self.assertIn("GITEA_TOKEN='s3cr3t v'", content) def test_write_seed_uses_atomic_write(self): # feat-chv-cover100:seed 暂存文件(user-data 含 host # 私钥明文)此前直接 open("w") 再 chmod 0600——崩溃 # 半截文件本身到不了 ISO(唯一消费者 genisoimage 只 # 在 write_seed 返回后跑,失败重试会整文件重写), # 但 chmod 之前文件以 umask 权限(常为 0644)可读, # 同 uid 进程能在该窗口读到私钥;现走 atomic_file #(mkstemp 0600 → 写 → fsync → rename),文件从 # 创建起就是 0600,且写失败不留半截文件。 import stat as _stat from unittest import mock import cloud.chv.hostkeys as hostkeys_mod from cloud.chv.atomic_file import atomic_write_text as _real calls = [] def _spy(path, text, mode=0o600): calls.append(path) return _real(path, text, mode=mode) with (tempfile.TemporaryDirectory() as tmp, mock.patch.object(hostkeys_mod, "atomic_write_text", side_effect=_spy)): seed_dir = os.path.join(tmp, "seed") write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx", network_config="version: 2\n") self.assertEqual( sorted(os.path.basename(p) for p in calls), ["meta-data", "network-config", "user-data"]) for name in ("meta-data", "user-data", "network-config"): mode = _stat.S_IMODE(os.stat( os.path.join(seed_dir, name)).st_mode) self.assertEqual(mode, 0o600, name) leftovers = [n for n in os.listdir(seed_dir) if n.startswith(".tmp-")] self.assertEqual(leftovers, []) def test_write_seed_failure_leaves_no_partial_file(self): # feat-chv-cover100:原子写的回归——写 user-data 中途 # 失败时不留半截 user-data、不留 tmp 残留;旧的直接 # 写会在崩溃后留半截文件(虽到不了 ISO,但含私钥明 # 文的半截文件滞留 seed 目录本身即泄漏面)。 from unittest import mock import cloud.chv.hostkeys as hostkeys_mod real = hostkeys_mod.atomic_write_text def _fail_on_user_data(path, text, mode=0o600): if path.endswith("user-data"): raise OSError("disk boom") return real(path, text, mode=mode) with (tempfile.TemporaryDirectory() as tmp, mock.patch.object(hostkeys_mod, "atomic_write_text", side_effect=_fail_on_user_data)): seed_dir = os.path.join(tmp, "seed") os.makedirs(seed_dir) with self.assertRaises(OSError): write_seed(seed_dir, "vm1", "PRIV", "ssh-ed25519 AAAAx") self.assertFalse(os.path.exists( os.path.join(seed_dir, "user-data"))) leftovers = [n for n in os.listdir(seed_dir) if n.startswith(".tmp-")] self.assertEqual(leftovers, []) def test_keyscan_is_weak_fallback_only(self): # provisioned 模式下 wake 前必须预置;keyscan 不应被 # 当成生产路径:配置值本身可解析,且文档/代码里标弱。 ex = self.make_exec(env_extra={"CHV_HOST_KEY_MODE": "provisioned"}) self.assertEqual(ex.host_key_mode, "provisioned") with self.assertRaises(ValueError): self.make_exec(env_extra={"CHV_HOST_KEY_MODE": "bogus"}) # -- (4) egress tap 校验 ------------------------------------------------- class EgressValidationTest(ChvGapCase): def test_direct_egress_without_flag_is_fail_closed(self): ex = self.make_exec() ex.egress_proxy = None ex.allow_direct_egress = False with self.assertRaises(ChvFailure) as ctx: ex.preflight() self.assertEqual(ctx.exception.kind, "egress_not_configured") def test_direct_egress_with_flag_passes_and_is_audited(self): ex = self.make_exec(env_extra={"CHV_ALLOW_DIRECT_EGRESS": "1"}) # preflight 本身不拦显式直连(守卫在 wake 的审计里)。 self.assertTrue(ex.preflight()) out = ex.goal_task({"id": "g1", "prompt": "做目标", "goal_mode": True}) self.assertEqual(out["status"], "completed") self.assertTrue(any(e == "egress_direct_allowed" for e, _, _, _ in self.audit)) def test_bad_net_json_is_preflight_error(self): ex = self.make_exec(env_extra={"CHV_NET_JSON": "{not json"}) with self.assertRaises(ChvFailure) as ctx: ex.preflight() self.assertEqual(ctx.exception.kind, "net_config_invalid") # -- (5) 构建脚本 -------------------------------------------------------- class BuildScriptTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.dir = self.tmp.name self.bin_dir = os.path.join(self.dir, "bin") os.makedirs(os.path.join(self.bin_dir, "codex-resources")) for name in ("codex", "codex-code-mode-host"): p = os.path.join(self.bin_dir, name) with open(p, "w") as fh: # B-09:build.sh 会核对 codex --version 含 CODEX_VERSION, # 假二进制须报出真实版本号。 fh.write("#!/bin/sh\necho \"codex-cli 0.164.0-alpha.9\"\n") os.chmod(p, 0o755) self.repo = os.path.join(self.dir, "repo") os.makedirs(os.path.join(self.repo, "cloud")) with open(os.path.join(self.repo, "host.py"), "w") as fh: fh.write("# host\n") # B-09:REPO_COMMIT 须等于仓库 HEAD 短 hash 且工作树干净, # 假仓库须是真 git 仓库。 subprocess.run(["git", "init", "-q", self.repo], check=True, capture_output=True) subprocess.run(["git", "-C", self.repo, "config", "user.email", "t@t"], check=True, capture_output=True) subprocess.run(["git", "-C", self.repo, "config", "user.name", "t"], check=True, capture_output=True) subprocess.run(["git", "-C", self.repo, "add", "-A"], check=True, capture_output=True) subprocess.run(["git", "-C", self.repo, "commit", "-qm", "init"], check=True, capture_output=True) self.repo_commit = subprocess.run( ["git", "-C", self.repo, "rev-parse", "--short", "HEAD"], check=True, capture_output=True, text=True).stdout.strip() self.script = str(Path(__file__).resolve().parents[2] / "chv-guest" / "build.sh") def tearDown(self): self.tmp.cleanup() def _run(self, args, env_extra=None): env = dict(os.environ) env.update({"CODEX_BIN_DIR": self.bin_dir, "UBUNTU_VERSION": "24.04", "CODEX_VERSION": "0.164.0-alpha.9", "REPO_COMMIT": self.repo_commit, # feat-chv-secfix(M4):SHA256 必填, # 干跑只验“已设”,用 FAKE 占位。 "UBUNTU_IMAGE_SHA256": "0" * 64, "REPO_DIR": self.repo}) if env_extra: env.update(env_extra) return subprocess.run(["bash", self.script] + args, capture_output=True, text=True, env=env) def test_dry_run_prints_real_steps_and_builds_nothing(self): out_dir = os.path.join(self.dir, "out") r = self._run(["--dry-run", out_dir]) self.assertEqual(r.returncode, 0, r.stderr) self.assertIn("qemu-img convert", r.stdout) self.assertIn("virt-customize", r.stdout) self.assertIn("virt-get-kernel", r.stdout) self.assertIn("BUILDINFO", r.stdout) self.assertIn("chv-guest-24.04-0.164.0-alpha.9-%s" % self.repo_commit, r.stdout) self.assertFalse(os.path.exists(os.path.join(out_dir, "disk.raw"))) def test_hostkey_dropin_generates_missing_keys_at_first_boot(self): # B-23:Minimal 底座的 cloud.cfg 不含 ssh 模块,build.sh # 曾写 ssh_genkeytypes: [] 彻底关掉首启 host key 生成, # 客机无 key、sshd -t 失败、ssh.service 连败 5 次,SSH # 全程不通。修正:首启 bootcmd 只在缺 key 时生成,绝不 # 重生成已预置的 per-VM key;写盘的命令里 [] 绝不再出现 #(注释里提历史可以,功能行里不行)。 with open(self.script) as fh: text = fh.read() hostkey_lines = [ln for ln in text.splitlines() if "99-chv-hostkey.cfg" in ln and "printf" in ln] self.assertEqual(len(hostkey_lines), 1) cmd = hostkey_lines[0] self.assertIn("ssh_deletekeys: false", cmd) self.assertIn("[ -f /etc/ssh/ssh_host_ed25519_key ] " "|| ssh-keygen -A", cmd) self.assertNotIn("ssh_genkeytypes", cmd) def test_full_mode_without_kvm_fails_fast(self): # 本 VM 容器内无 /dev/kvm,全量模式必须明确失败, # 绝不声称已建成镜像。 r = self._run([os.path.join(self.dir, "out2")]) self.assertNotEqual(r.returncode, 0) self.assertIn("/dev/kvm", r.stderr + r.stdout) def test_missing_binary_fails_fast(self): os.remove(os.path.join(self.bin_dir, "codex")) r = self._run(["--dry-run", os.path.join(self.dir, "out3")]) self.assertNotEqual(r.returncode, 0) # -- (5b) chv-sync 双向同步 ------------------------------------------------ # 轻量口径 + Gitea 集成(2026-11-06):本地 bare 仓当远端替身, # 不碰真实 Gitea、不需 token,测同步机制本身(双向通 + 分叉/ # 脏树时失败关闭)。boot 的 clone 一步需 root + guest-env, # 不在单元测试里跑,只测 boot 无 guest-env 时静默成功与 # guest-env 缺字段时明确失败;真机验收见构建记录。 class ChvSyncTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.dir = self.tmp.name self.script = str(Path(__file__).resolve().parents[2] / "chv-guest" / "chv-sync.sh") self.ws = os.path.join(self.dir, "workspace") os.makedirs(self.ws) # 远端替身:bare 仓 + 一个已推入的初始提交。 self.remote = os.path.join(self.dir, "remote.git") subprocess.run(["git", "init", "-q", "--bare", self.remote], check=True, capture_output=True) seed = os.path.join(self.dir, "seed-clone") subprocess.run(["git", "clone", "-q", self.remote, seed], check=True, capture_output=True) self._git(seed, "config", "user.email", "t@t") self._git(seed, "config", "user.name", "t") with open(os.path.join(seed, "a.txt"), "w") as fh: fh.write("v1\n") self._git(seed, "add", "-A") self._git(seed, "commit", "-qm", "v1") self._git(seed, "push", "-q", "origin", "HEAD:main") # 本地工作区:模拟 boot 已 clone 的结果。 self.repo = os.path.join(self.ws, "remote") subprocess.run(["git", "clone", "-q", "--branch", "main", self.remote, self.repo], check=True, capture_output=True) self._git(self.repo, "config", "user.email", "t@t") self._git(self.repo, "config", "user.name", "t") def tearDown(self): self.tmp.cleanup() def _git(self, cwd, *args): return subprocess.run(["git", "-C", cwd] + list(args), check=True, capture_output=True, text=True) def _sync(self, *args, env_extra=None): env = dict(os.environ) env["CHV_WORKSPACE"] = self.ws env["CHV_GUEST_ENV"] = os.path.join(self.dir, "no-guest-env") if env_extra: env.update(env_extra) return subprocess.run(["bash", self.script] + list(args), capture_output=True, text=True, env=env) def _head(self, cwd): return self._git(cwd, "rev-parse", "HEAD").stdout.strip() def test_push_then_pull_round_trip(self): # 本地改 → push → 远端可见。 with open(os.path.join(self.repo, "a.txt"), "a") as fh: fh.write("v2-local\n") self._git(self.repo, "add", "-A") self._git(self.repo, "commit", "-qm", "v2-local") r = self._sync("push", self.repo) self.assertEqual(r.returncode, 0, r.stderr) other = os.path.join(self.dir, "other-clone") subprocess.run(["git", "clone", "-q", "--branch", "main", self.remote, other], check=True, capture_output=True) self.assertEqual(self._head(other), self._head(self.repo)) # 远端改 → pull → 本地同步回来。 self._git(other, "config", "user.email", "t@t") self._git(other, "config", "user.name", "t") with open(os.path.join(other, "a.txt"), "a") as fh: fh.write("v3-remote\n") self._git(other, "add", "-A") self._git(other, "commit", "-qm", "v3-remote") self._git(other, "push", "-q", "origin", "HEAD:main") r = self._sync("pull", self.repo) self.assertEqual(r.returncode, 0, r.stderr) with open(os.path.join(self.repo, "a.txt")) as fh: self.assertIn("v3-remote", fh.read()) def test_push_new_branch_sets_upstream(self): self._git(self.repo, "checkout", "-qb", "feature") with open(os.path.join(self.repo, "b.txt"), "w") as fh: fh.write("f\n") self._git(self.repo, "add", "-A") self._git(self.repo, "commit", "-qm", "feature") r = self._sync("push", self.repo) self.assertEqual(r.returncode, 0, r.stderr) out = subprocess.run(["git", "--git-dir", self.remote, "branch", "--list", "feature"], check=True, capture_output=True, text=True).stdout self.assertIn("feature", out) def test_diverged_pull_fails_closed(self): # 本地与远端同分支各进一步 → pull 必须失败,不自动合并。 with open(os.path.join(self.repo, "a.txt"), "a") as fh: fh.write("local-diverge\n") self._git(self.repo, "add", "-A") self._git(self.repo, "commit", "-qm", "local-diverge") other = os.path.join(self.dir, "other2") subprocess.run(["git", "clone", "-q", "--branch", "main", self.remote, other], check=True, capture_output=True) self._git(other, "config", "user.email", "t@t") self._git(other, "config", "user.name", "t") with open(os.path.join(other, "a.txt"), "a") as fh: fh.write("remote-diverge\n") self._git(other, "add", "-A") self._git(other, "commit", "-qm", "remote-diverge") self._git(other, "push", "-q", "origin", "HEAD:main") r = self._sync("pull", self.repo) self.assertNotEqual(r.returncode, 0) with open(os.path.join(self.repo, "a.txt")) as fh: self.assertNotIn("remote-diverge", fh.read()) def test_dirty_tree_pull_refused(self): with open(os.path.join(self.repo, "a.txt"), "a") as fh: fh.write("uncommitted\n") r = self._sync("pull", self.repo) self.assertNotEqual(r.returncode, 0) def test_boot_without_guest_env_is_noop(self): r = self._sync("boot") self.assertEqual(r.returncode, 0, r.stderr) def test_boot_with_incomplete_guest_env_fails(self): env_file = os.path.join(self.dir, "guest-env") with open(env_file, "w") as fh: fh.write("GITEA_URL=https://gitea.tose.top\n") r = self._sync("boot", env_extra={"CHV_GUEST_ENV": env_file}) self.assertNotEqual(r.returncode, 0) # -- (6) 中途死亡 → interrupted → 断点续跑闭环 --------------------------- class MidDeathLoopTest(ChvGapCase): def test_vm_death_is_interrupted_then_breakpoint_resumes(self): state = {"dead": False} def runner(cmd, timeout=None): self.calls.append(list(cmd)) if cmd and cmd[0] == "ssh": if cmd[-1] == "true": return FakeResult(0, "", "") if "goal-resume" in cmd and state["dead"]: # 第二次断点调用:VM 已换新,健康返回。 return FakeResult(0, "goal met after rounds\n", "") if "goal" in cmd: # 第一次 goal:模拟传输中断,并把 VMM 进程标死。 state["dead"] = True # 找到登记的 VMM 进程并标死(wake 已登记)。 for proc in list(ex._vms.values()): proc._alive = False raise RuntimeError("connection lost mid-task") return FakeResult(0, "", "") ex = self.make_exec(runner=runner) # 第一次 goal:中途死应判 interrupted,不是 failed 死信。 out = ex.goal_task({"id": "g9", "prompt": "做目标", "goal_mode": True}) self.assertEqual(out["status"], "interrupted") # 断点续跑:同一执行器的断点方法能走通并完成。 state["dead"] = True # 标记已进入断点阶段(runner 按此分支) out2 = ex.goal_resume_breakpoint_task( {"id": "g9", "prompt": "续跑"}) self.assertEqual(out2["status"], "completed") if __name__ == "__main__": unittest.main()