"""对齐 Muse round-12 集成测试(P4~P12 合入后的跨轨接线)。 只测本轨新增的接线,不复述各轨自有测试: - AppStateStore KV API + audit_trail 表(迁移地基)。 - P8 三处登记 + P12 审计 JSONL 到 AppStateStore 的迁移 (语义不变:去重、哈希链、project_id 隔离;旧文件一次 性导入;损坏回空 fail-safe)。 - P6 worker → P3 helper/authd → git 的本地协议链(真 PrivsepClient + 真 worker server + 真 git status;runuser 与 HTTP 拉取需真机,见测试 docstring)。 - P12 审批卡 / 权限登记对真实 P3/P5 代码的 dict 契约 (不再用替身)。 """ import importlib.util import json import os import sqlite3 import subprocess import tempfile import threading import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] from cloud.chv.app_state import AppStateStore, host_state_db_path from cloud.chv.audit_trail import ( AuditTrail, StoreAuditTrail, migrate_jsonl_to_store, ) from cloud.chv.browser_tasks import ( BrowserTransport, create_instance, get_instance, ) from cloud.chv.presentation import NotificationLedger from cloud.chv.swarm import ( create_swarm, get_swarm, ) TOKEN = "fake-token-123" T0 = 1700000000.0 def _load_guest_module(filename, module_name): """按文件加载 chv-guest 下的模块(不走包名,与 P3 测试同款)。""" path = REPO_ROOT / "chv-guest" / filename spec = importlib.util.spec_from_file_location(module_name, path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod class KvApiTest(unittest.TestCase): """AppStateStore KV API(P8 迁移的后端)。""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.dir = self.tmp.name self.store = AppStateStore( os.path.join(self.dir, "app-state.sqlite")) self.store.open() def tearDown(self): self.store.close() self.tmp.cleanup() def test_put_get_delete_roundtrip(self): self.assertIsNone(self.store.kv_get("p8-swarm", "registry")) self.store.kv_put("p8-swarm", "registry", '{"a": 1}') self.assertEqual(self.store.kv_get("p8-swarm", "registry"), '{"a": 1}') self.store.kv_put("p8-swarm", "registry", '{"a": 2}') self.assertEqual(self.store.kv_get("p8-swarm", "registry"), '{"a": 2}') self.store.kv_delete("p8-swarm", "registry") self.assertIsNone(self.store.kv_get("p8-swarm", "registry")) self.store.kv_delete("p8-swarm", "registry") def test_namespaces_isolated(self): self.store.kv_put("p8-swarm", "registry", "swarm-doc") self.store.kv_put("p8-ledger", "registry", "ledger-doc") self.assertEqual(self.store.kv_get("p8-swarm", "registry"), "swarm-doc") self.assertEqual(self.store.kv_get("p8-ledger", "registry"), "ledger-doc") def test_bad_namespace_or_key_rejected(self): for ns, key in (("", "k"), ("a:b", "k"), ("ns", ""), ("ns", "a:b")): with self.assertRaises(ValueError): self.store.kv_put(ns, key, "v") with self.assertRaises(ValueError): self.store.kv_put("ns", "k", 123) def test_host_path_differs_from_vm_path(self): from cloud.chv.app_state import state_db_path host = host_state_db_path(self.dir) vm = state_db_path(self.dir, "chv-abc123") self.assertNotEqual(host, vm) self.assertTrue(host.endswith("app-state.sqlite")) self.assertEqual(os.path.dirname(host), self.dir) def test_counts_includes_audit_trail(self): counts = self.store.counts() self.assertIn("audit_trail", counts) self.assertEqual(counts["audit_trail"], 0) class StoreAuditTrailTest(unittest.TestCase): """StoreAuditTrail:与 JSONL 版同款链与隔离,走 SQLite。""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.dir = self.tmp.name def tearDown(self): self.tmp.cleanup() def test_append_chain_resumes_across_instances(self): first = StoreAuditTrail(self.dir) r1 = first.append("p1", "worker", "task_dispatch", "run", object_id="t1", ts=T0) r2 = first.append("p1", "worker", "vm_lifecycle", "wake", object_id="vm1", ts=T0 + 1) self.assertEqual(r1["seq"], 1) self.assertEqual(r2["seq"], 2) self.assertEqual(r2["prev_hash"], r1["hash"]) second = StoreAuditTrail(self.dir) r3 = second.append("p1", "worker", "task_dispatch", "done", object_id="t1", ts=T0 + 2) self.assertEqual(r3["seq"], 3) self.assertEqual(r3["prev_hash"], r2["hash"]) ok, bad = second.verify() self.assertTrue(ok) self.assertEqual(bad, -1) def test_project_isolation_and_filters(self): trail = StoreAuditTrail(self.dir) trail.append("p1", "worker", "task_dispatch", "run", object_id="t1", ts=T0) trail.append("p2", "worker", "task_dispatch", "run", object_id="t2", ts=T0) trail.append("p1", "worker", "quota", "denied", object_id="t1", ts=T0 + 1) self.assertEqual(len(trail.list_records("p1", limit=0)), 2) self.assertEqual(len(trail.list_records("p2", limit=0)), 1) self.assertEqual(trail.list_records("p3", limit=0), []) self.assertEqual(trail.list_records("", limit=0), []) self.assertEqual( len(trail.list_records("p1", category="quota", limit=0)), 1) rows = trail.list_records("p1", limit=0) self.assertGreater(rows[0]["ts"], rows[1]["ts"]) def test_tamper_detected_with_row_index(self): trail = StoreAuditTrail(self.dir) trail.append("p1", "worker", "task_dispatch", "run", object_id="t1", ts=T0) trail.append("p1", "worker", "task_dispatch", "done", object_id="t1", ts=T0 + 1) db = host_state_db_path(self.dir) conn = sqlite3.connect(db) conn.execute("UPDATE audit_trail SET action = ? WHERE seq = 2", ("forged",)) conn.commit() conn.close() ok, bad = StoreAuditTrail(self.dir).verify() self.assertFalse(ok) self.assertEqual(bad, 1) def test_export_json(self): trail = StoreAuditTrail(self.dir) trail.append("p1", "worker", "task_dispatch", "run", object_id="t1", ts=T0) trail.append("p1", "worker", "task_dispatch", "done", object_id="t1", ts=T0 + 1) dest = os.path.join(self.dir, "export.json") self.assertEqual(trail.export_json("p1", dest), 2) self.assertEqual(os.stat(dest).st_mode & 0o777, 0o600) with open(dest) as fh: rows = json.load(fh) self.assertEqual([r["seq"] for r in rows], [1, 2]) def test_secret_detail_scrubbed(self): trail = StoreAuditTrail(self.dir) rec = trail.append("p1", "worker", "task_dispatch", "run", detail={"token": TOKEN, "note": "ok"}, ts=T0) self.assertNotIn("token", rec["detail"]) self.assertEqual(rec["detail"]["note"], "ok") class MigrateJsonlTest(unittest.TestCase): """migrate_jsonl_to_store:原样搬链。""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.dir = self.tmp.name self.jsonl = os.path.join(self.dir, "audit.jsonl") def tearDown(self): self.tmp.cleanup() def _seed_jsonl(self): trail = AuditTrail(self.jsonl) trail.append("p1", "worker", "task_dispatch", "run", object_id="t1", ts=T0) trail.append("p2", "worker", "quota", "denied", object_id="t2", ts=T0 + 1) trail.append("p1", "worker", "task_dispatch", "done", object_id="t1", ts=T0 + 2) return trail def test_migrate_then_verify_and_list(self): self._seed_jsonl() self.assertEqual(migrate_jsonl_to_store(self.jsonl, self.dir), 3) store_trail = StoreAuditTrail(self.dir) ok, bad = store_trail.verify() self.assertTrue(ok) self.assertEqual(bad, -1) self.assertEqual(len(store_trail.list_records("p1", limit=0)), 2) self.assertEqual(len(store_trail.list_records("p2", limit=0)), 1) rows = store_trail.list_records("p1", limit=0) self.assertEqual([r["seq"] for r in rows], [3, 1]) def test_migrate_is_idempotent(self): self._seed_jsonl() self.assertEqual(migrate_jsonl_to_store(self.jsonl, self.dir), 3) self.assertEqual(migrate_jsonl_to_store(self.jsonl, self.dir), 0) ok, _ = StoreAuditTrail(self.dir).verify() self.assertTrue(ok) def test_missing_file_returns_zero(self): self.assertEqual( migrate_jsonl_to_store(os.path.join(self.dir, "nope"), self.dir), 0) def test_broken_chain_refused(self): self._seed_jsonl() with open(self.jsonl, "a") as fh: fh.write("{broken json\n") with self.assertRaises(ValueError): migrate_jsonl_to_store(self.jsonl, self.dir) self.assertEqual( len(StoreAuditTrail(self.dir).list_records("p1", limit=0)), 0) def test_divergent_seq_refused(self): self._seed_jsonl() migrate_jsonl_to_store(self.jsonl, self.dir) other = os.path.join(self.dir, "other.jsonl") trail = AuditTrail(other) trail.append("p1", "worker", "task_dispatch", "run", object_id="tX", ts=T0) with self.assertRaises(ValueError): migrate_jsonl_to_store(other, self.dir) class P8MigrationTest(unittest.TestCase): """P8 三处登记走 KV:无 JSON 文件、旧文件导入、损坏恢复。""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.dir = self.tmp.name def tearDown(self): self.tmp.cleanup() def test_swarm_uses_kv_no_json_file(self): swarm = create_swarm(self.dir, "p1", "parent1", now=T0) self.assertFalse(os.path.exists( os.path.join(self.dir, "swarms.json"))) self.assertTrue(os.path.exists(host_state_db_path(self.dir))) with AppStateStore(host_state_db_path(self.dir)) as store: raw = store.kv_get("p8-swarm", "registry") self.assertIsNotNone(raw) self.assertIn(swarm["id"], json.loads(raw)["swarms"]) self.assertEqual(get_swarm(self.dir, swarm["id"])["id"], swarm["id"]) def test_swarm_legacy_json_imported_once(self): legacy = {"swarms": {"s1": {"id": "s1", "project_id": "p1", "parent_task_id": "parent1", "agents": {}, "status": "active", "created_at": T0}}, "tools": {}, "invocations": [], "audit": []} with open(os.path.join(self.dir, "swarms.json"), "w") as fh: json.dump(legacy, fh) self.assertEqual(get_swarm(self.dir, "s1")["id"], "s1") self.assertTrue(os.path.exists( os.path.join(self.dir, "swarms.json.migrated"))) self.assertFalse(os.path.exists( os.path.join(self.dir, "swarms.json"))) def test_swarm_corrupt_row_recovers(self): create_swarm(self.dir, "p1", "parent1", now=T0) with AppStateStore(host_state_db_path(self.dir)) as store: store.kv_put("p8-swarm", "registry", "{not json") swarm = create_swarm(self.dir, "p1", "parent2", now=T0) self.assertEqual(swarm["parent_task_id"], "parent2") def test_ledger_uses_kv_no_json_file(self): ledger = NotificationLedger(self.dir) self.assertTrue(ledger.should_notify("t1", "failed", now=T0)) self.assertFalse(os.path.exists(os.path.join( self.dir, "notification-ledger.json"))) with AppStateStore(host_state_db_path(self.dir)) as store: raw = store.kv_get("p8-ledger", "registry") self.assertIn("t1:failed", json.loads(raw)["notified"]) def test_browser_tasks_legacy_import_and_corrupt_row(self): legacy = {"instances": { "old-task": {"instance_id": "i1", "task_id": "old-task", "status": "destroyed", "shots_dir": "browser-tasks/i1/shots", "created_at": T0}}} with open(os.path.join(self.dir, "browser-tasks.json"), "w") as fh: json.dump(legacy, fh) self.assertEqual( get_instance(self.dir, "old-task")["instance_id"], "i1") self.assertTrue(os.path.exists(os.path.join( self.dir, "browser-tasks.json.migrated"))) class _FakeTransport(BrowserTransport): def spawn(self, instance): return None def close(self, instance): return None def ping(self, instance): return True with AppStateStore(host_state_db_path(self.dir)) as store: store.kv_put("p8-browser-tasks", "registry", "{not json") inst = create_instance(self.dir, "new-task", _FakeTransport(), now=T0) self.assertEqual(inst["status"], "active") self.assertFalse(os.path.exists(os.path.join( self.dir, "browser-tasks.json"))) class WorkerProtocolTest(unittest.TestCase): """P6 worker → P3 helper/authd → git 本地协议链。 真 PrivsepClient + 真 chv-gitea-worker server(Unix socket + SO_PEERCRED)+ 真 git status;run_git 探针调 worker 自身 的 ``_git_base_argv()``(含 P3 helper 与 useHttpPath)组 argv,只去掉本机不存在的 ``runuser -u chv`` 包装——runuser 与 HTTP 拉取(需真实 gitea.tose.top + Sentinel 放行)留 给真机轨,本测试不假装已覆盖。 """ HELPER = REPO_ROOT / "chv-guest" / "git-credential-chv-authd" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.dir = self.tmp.name self.workspace = os.path.join(self.dir, "workspace") os.makedirs(self.workspace) self.repo = os.path.join(self.workspace, "testrepo") subprocess.run(["git", "init", "-q", "-b", "main", self.repo], check=True) subprocess.run(["git", "-C", self.repo, "config", "user.email", "t@example.invalid"], check=True) subprocess.run(["git", "-C", self.repo, "config", "user.name", "Test"], check=True) with open(os.path.join(self.repo, "README.md"), "w") as fh: fh.write("hello\n") subprocess.run(["git", "-C", self.repo, "add", "."], check=True) subprocess.run(["git", "-C", self.repo, "commit", "-q", "-m", "init"], check=True) self.socket_path = os.path.join(self.dir, "worker.sock") self.log_path = os.path.join(self.dir, "worker.log") self.mod = _load_guest_module("chv-gitea-worker.py", "chv_gitea_worker_round12") self.mod.HELPER_PATH = str(self.HELPER) self.argv_seen = [] self.env_seen = [] def run_git(action, repo): argv = ["runuser", "-u", self.mod.SYNC_USER, "--", "env", "HOME=/home/chv"] + \ self.mod._git_base_argv() if action == "status": argv += ["-C", repo, "status", "-sb"] elif action == "pull": argv += ["-C", repo, "pull", "--ff-only"] elif action == "push": argv += ["-C", repo, "push"] else: return 2, "", "未知 git 操作:%s" % action self.argv_seen.append(list(argv)) env = {"PATH": os.environ.get("PATH", ""), "HOME": self.dir, "CHV_AUTHD_SOCKET": ""} self.env_seen.append(dict(env)) runnable = [a for a in argv if a not in ("runuser", "-u", "chv", "--")] runnable[1] = "HOME=%s" % self.dir proc = subprocess.run(runnable, capture_output=True, text=True, timeout=60, env={**env, "HOME": self.dir}) return proc.returncode, proc.stdout, proc.stderr self.server = self.mod.WorkerServer( self.socket_path, self.workspace, self.log_path, allowed_uids=(os.getuid(),), run_git=run_git, helper_path=str(self.HELPER)) self.thread = threading.Thread( target=self.server.serve_forever, daemon=True) self.thread.start() def tearDown(self): self.server.shutdown() self.server.server_close() self.tmp.cleanup() def test_status_through_real_client_and_server(self): from cloud.chv.privsep import PrivsepClient, WorkerRequest client = PrivsepClient(socket_path=self.socket_path, connector="gitea", workspace=self.workspace) ping = client.ping() self.assertTrue(ping.ok) resp = client.call(WorkerRequest( "status", "gitea", self.repo, "req-1")) self.assertTrue(resp.ok, resp.stderr) self.assertEqual(resp.returncode, 0) self.assertIn("## main", resp.stdout) self.assertEqual(resp.request_id, "req-1") # worker 的 git argv 走 P3 helper + useHttpPath,不含 token: self.assertEqual(len(self.argv_seen), 1) argv = self.argv_seen[0] self.assertIn("credential.helper=%s" % self.HELPER, argv) self.assertIn("credential.useHttpPath=true", argv) self.assertNotIn(TOKEN, " ".join(argv)) self.assertNotIn(TOKEN, json.dumps(self.env_seen)) # worker 用的 helper 就是 P3 的 helper 文件本身: self.assertTrue(os.path.samefile( self.server.helper_path, str(self.HELPER))) # 审计日志不含 token(本测试的 token 只存在于断言字面量, # 不进任何被测输入——worker 从未见过它): with open(self.log_path) as fh: self.assertNotIn(TOKEN, fh.read()) def test_helper_file_is_the_p3_helper(self): # worker 硬编码的 helper 路径与 P3 测试用的 helper 是同一 # 个文件(接线不是替身)。 from cloud.tests.test_chv_muse_p3 import HelperE2ETest self.assertTrue(os.path.samefile( str(self.HELPER), str(HelperE2ETest.HELPER))) class P12DictContractTest(unittest.TestCase): """P12 接线用真实 P3/P5 代码,不用替身。""" def test_approval_card_from_real_p3_queue(self): from cloud.chv.approval_card import ( build_approval_card, card_matches_channel, ) from cloud.chv.approval_channel import request_approval from cloud.chv.sentinel import ApprovalQueue, EgressRequest queue = ApprovalQueue(clock=lambda: T0) req = EgressRequest("gitea", "POST", "https", "gitea.tose.top", 443, "/api/v1/repos/admin/x/issues") pending_id = queue.record(req, ["needs_approval"]) record = queue.list_pending()[0] self.assertEqual(record["pending_id"], pending_id) card = build_approval_card(record) self.assertEqual(card["pending_id"], pending_id) self.assertEqual(card["target"]["host"], "gitea.tose.top") self.assertEqual(card["target"]["method"], "POST") self.assertEqual(card["risk"], "medium") self.assertIn("needs_approval", card["reasons"]) channel = request_approval(record, secret="test-secret", now=T0) self.assertTrue( card_matches_channel(card, channel.payload())) def test_permission_registry_from_real_p5_describe(self): from cloud.chv.activity import PermissionRegistry from cloud.chv.capabilities import CapabilityIssuer issuer = CapabilityIssuer(clock=lambda: T0) cap = issuer.issue( "one-shot", "gitea", "gitea.tose.top", ["GET"], [("GET", "/api/v1/*", "allow")], "sync", now=T0) described = cap.describe() reg_path = os.path.join( tempfile.mkdtemp(), "permissions.json") reg = PermissionRegistry(reg_path) grant = reg.record_grant("p1", described, ts=T0) self.assertEqual(grant["capability_id"], described["capability_id"]) self.assertEqual(grant["connector"], "gitea") self.assertEqual(grant["host"], "gitea.tose.top") self.assertEqual(grant["use_case"], "sync") self.assertEqual(grant["tier"], "one-shot") active = reg.list_active("p1", now=T0 + 10) self.assertEqual(len(active), 1) self.assertEqual(active[0]["capability_id"], described["capability_id"]) self.assertTrue(reg.note_use("p1", described["capability_id"])) self.assertTrue(reg.revoke("p1", described["capability_id"])) self.assertEqual(reg.list_active("p1", now=T0 + 10), []) if __name__ == "__main__": unittest.main()