"""CHV 混合模式 B 轨(feat-chv-hybrid-b)测试。 只用标准库与 mock runner/launcher/api_client,绝不启动真实 VM(与 test_chv_opt_stage1.py 同款边界)。覆盖(设计 DESIGN-HYBRID.md §9.1 第 5–9 项): - 登记字段扩展 + tier 校验(含无 tier 行拒绝常驻) - 租约挂起/降级计时(环境变量注入短阈值测状态机转换, 含降级同步失败中止) - 排队超时拒绝(host_capacity_exceeded,不插队、不抢占、 不升降档) - 升级提示点去重(一周内同一租户最多一次) - 网关分流(常驻可接、池不接) """ import json import os import sys import tempfile import unittest from pathlib import Path from unittest import mock try: from cloud.chv import ChvFailure from cloud.chv import CloudHypervisorExecutor from cloud.chv.disk import ( read_persistent_vm, write_persistent_vm, write_vm_tenant, ) except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.chv import ChvFailure from cloud.chv import CloudHypervisorExecutor from cloud.chv.disk import ( read_persistent_vm, write_persistent_vm, write_vm_tenant, ) 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 HybridBCase(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, "base.qcow2"), 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.launches = [] self.vm_states = {} self.sleeps = [] self.events = [] def tearDown(self): self.tmp.cleanup() def make_exec(self, runner=None, api_client=None, launcher=None, env_extra=None, sleeper=None, **overrides): env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888", "CHV_HOST_KEY_MODE": "keyscan", "CHV_SSH_FORWARD": "1", "CHV_HOST_ID": "host-test-1"} if env_extra: env.update(env_extra) if runner is None: runner = self._default_runner if api_client is None: api_client = self._default_api if launcher is None: launcher = self._default_launcher 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, launcher=launcher, api_client=api_client, sleeper=sleeper if sleeper is not None else (lambda s: self.sleeps.append(s)), env=env) kwargs.update(overrides) ex = CloudHypervisorExecutor(**kwargs) ex.audit_callback = lambda event, task, detail, kind=None: \ self.events.append((event, detail, kind)) return ex def _default_runner(self, cmd, timeout=None): self.calls.append(list(cmd)) if cmd[0] == "ssh-keyscan": return FakeResult( 0, "[127.0.0.1]:2222 ssh-ed25519 AAAAB3test\n", "") if cmd[0] == "ssh": if cmd[-1] == "true": return FakeResult(0, "", "") if "dispatch" in cmd: return FakeResult(0, json.dumps( {"status": "completed", "text": "ok-dispatch"}), "") return FakeResult(0, "", "") if cmd and os.path.basename(str(cmd[0])) == "ch-remote": if "snapshot" in cmd: url = cmd[cmd.index("snapshot") + 1] if url.startswith("file://"): snap_dir = url[len("file://"):] os.makedirs(snap_dir, exist_ok=True) with open(os.path.join(snap_dir, "state.json"), "w") as fh: fh.write("{}") return FakeResult(0, "", "") return FakeResult(0, "", "") return FakeResult(0, "", "") def _default_api(self, socket_path, method, path, payload=None): self.api_calls.append((method, path, payload)) if path == "/vm.info": state = self.vm_states.get( os.path.basename(socket_path)[:-len(".sock")]) if state: return (200, {"state": state}) return (200, {}) return (200, {}) def _default_launcher(self, cmd): self.launches.append(list(cmd)) return FakeProc(alive=True) def _task(self, task_id="abc123", project="proj1"): return {"id": task_id, "prompt": "做一个加法函数", "project_id": project} def _event_names(self): return [e[0] for e in self.events] def _write_resident(self, ex, tenant, state, tier, last_active, vm=None): vm = vm or ex.persistent_vm_name(tenant) write_persistent_vm(ex.state_dir, tenant, { "vm": vm, "tenant_id": tenant, "tier": tier, "state": state, "last_active_at": last_active, "lease_expires_at": last_active + 1800 + 86400, "origin": "task", "gitea_synced": True, "updated_at": last_active}) return vm def _live_vm(self, ex, vm): """给一台 VM 造“活着”的外形:socket 文件 + vm.info。""" sock = os.path.join(ex.state_dir, vm + ".sock") os.makedirs(ex.state_dir, exist_ok=True) with open(sock, "w") as fh: fh.write("") return sock # -- 登记字段扩展 + tier 校验(第 5 项)------------------------------- class ResidentRegistryTest(HybridBCase): def test_promote_writes_extended_fields(self): ex = self.make_exec() out = ex.promote_to_resident("t1", "Paid") self.assertTrue(out["promoted"]) self.assertFalse(out["queued"]) record = read_persistent_vm(ex.state_dir, "t1") self.assertEqual(record["tier"], "Paid") self.assertEqual(record["state"], "resident-active") self.assertIn("last_active_at", record) self.assertIn("lease_expires_at", record) self.assertGreater(record["lease_expires_at"], record["last_active_at"]) self.assertIn("vm_resident_promoted", self._event_names()) detail = [d for e, d, _k in self.events if e == "vm_resident_promoted"][0] self.assertIn("host_id=host-test-1", detail) self.assertIn("tenant=t1", detail) def test_promote_free_tier_denied(self): ex = self.make_exec() with self.assertRaises(ChvFailure) as ctx: ex.promote_to_resident("tfree", "Free") self.assertEqual(ctx.exception.kind, "tier_denied") self.assertIsNone(read_persistent_vm(ex.state_dir, "tfree")) def test_promote_no_tier_row_denied(self): # 无 tier 行 = 无常驻资格(fail-closed,走池):登记里 # 没有 tier 字段、也不传 tier 参数时同样拒绝。 ex = self.make_exec() with self.assertRaises(ChvFailure) as ctx: ex.promote_to_resident("tnope") self.assertEqual(ctx.exception.kind, "tier_denied") def test_tier_eligible_matrix(self): ex = self.make_exec() self.assertTrue(ex.resident_tier_eligible("x", "Paid")) self.assertTrue(ex.resident_tier_eligible("x", "Heavy")) self.assertFalse(ex.resident_tier_eligible("x", "Free")) self.assertFalse(ex.resident_tier_eligible("x", None)) self.assertFalse(ex.resident_tier_eligible("nobody")) def test_promote_over_cap_queued_not_preempt(self): # max_vms 默认 4 → 常驻上限 2;第 3 个排队,不抢占前两个。 ex = self.make_exec() self.assertTrue(ex.promote_to_resident("t1", "Paid")["promoted"]) self.assertTrue(ex.promote_to_resident("t2", "Heavy")["promoted"]) out = ex.promote_to_resident("t3", "Paid") self.assertFalse(out["promoted"]) self.assertTrue(out["queued"]) self.assertEqual(out["reason"], "resident_capacity_exceeded") self.assertEqual(ex._resident_count(), 2) self.assertEqual( read_persistent_vm(ex.state_dir, "t1")["state"], "resident-active") self.assertEqual( read_persistent_vm(ex.state_dir, "t2")["state"], "resident-active") self.assertIsNone(read_persistent_vm(ex.state_dir, "t3")) def test_promote_idempotent_no_double_slot(self): ex = self.make_exec() ex.promote_to_resident("t1", "Paid") out = ex.promote_to_resident("t1", "Paid") self.assertTrue(out["promoted"]) self.assertEqual(ex._resident_count(), 1) def test_touch_renews_only_resident(self): ex = self.make_exec(env_extra={ "CHV_RESIDENT_IDLE_SUSPEND_SECONDS": "60", "CHV_RESIDENT_IDLE_DEMOTE_SECONDS": "120"}) now = 1800000000.0 # legacy paused 登记:touch 不写、不造租约字段。 write_persistent_vm(ex.state_dir, "tleg", { "vm": ex.persistent_vm_name("tleg"), "tenant_id": "tleg", "state": "paused", "origin": "task", "gitea_synced": True, "updated_at": now - 100}) self.assertFalse(ex.touch_resident_activity("tleg", now=now)) self.assertNotIn("last_active_at", read_persistent_vm(ex.state_dir, "tleg")) # 常驻登记:touch 续期并重算 lease_expires_at。 vm = self._write_resident(ex, "t2", "resident-active", "Paid", now - 100) self.assertTrue(ex.touch_resident_activity("t2", now=now)) record = read_persistent_vm(ex.state_dir, "t2") self.assertEqual(record["last_active_at"], now) self.assertEqual(record["lease_expires_at"], now + 60 + 120) self.assertEqual(record["state"], "resident-active") self.assertEqual(record["tier"], "Paid") def test_suspend_after_task_keeps_resident_state_and_tier(self): ex = self.make_exec() ex.promote_to_resident("t1", "Paid") vm = ex.persistent_vm_name("t1") self.assertTrue(ex.suspend_after_task({"id": vm}, tenant_id="t1")) record = read_persistent_vm(ex.state_dir, "t1") self.assertEqual(record["state"], "resident-active") self.assertEqual(record["tier"], "Paid") self.assertIn("last_active_at", record) def test_acquire_resident_paused_paid_tier(self): ex = self.make_exec(env_extra={"CHV_WARM_POOL_SIZE": "1"}) now = 1800000000.0 vm = self._write_resident(ex, "t1", "resident-paused", "Paid", now - 10) write_vm_tenant(ex.state_dir, vm, "t1") self._live_vm(ex, vm) self.vm_states[vm] = "Paused" out = ex.acquire_for_task(self._task(project="t1")) self.assertEqual(out["mode"], "resumed") self.assertEqual(out["vm"], vm) record = read_persistent_vm(ex.state_dir, "t1") # resume 后仍是常驻态(不写回 legacy running),tier 保留。 self.assertEqual(record["state"], "resident-paused") self.assertEqual(record["tier"], "Paid") def test_acquire_resident_paused_free_tier_denied(self): ex = self.make_exec(env_extra={"CHV_WARM_POOL_SIZE": "1"}) now = 1800000000.0 vm = self._write_resident(ex, "t1", "resident-paused", "Free", now - 10) write_vm_tenant(ex.state_dir, vm, "t1") self._live_vm(ex, vm) self.vm_states[vm] = "Paused" out = ex.acquire_for_task(self._task(project="t1")) self.assertEqual(out["mode"], "cold") self.assertIn("resident_tier_denied", self._event_names()) record = read_persistent_vm(ex.state_dir, "t1") self.assertEqual(record["state"], "resident-paused") self.assertEqual(record["tier"], "Free") def test_acquire_legacy_paused_without_tier_still_works(self): # legacy paused 登记无 tier 字段:走旧语义,仍可认领。 ex = self.make_exec(env_extra={"CHV_WARM_POOL_SIZE": "1"}) vm = ex.persistent_vm_name("tleg") write_persistent_vm(ex.state_dir, "tleg", { "vm": vm, "tenant_id": "tleg", "state": "paused", "origin": "prewarm", "gitea_synced": True, "updated_at": 1800000000.0}) write_vm_tenant(ex.state_dir, vm, "tleg") self._live_vm(ex, vm) self.vm_states[vm] = "Paused" out = ex.acquire_for_task(self._task(project="tleg")) self.assertEqual(out["mode"], "resumed") # -- 租约挂起/降级计时(第 6 项)-------------------------------------- class LeaseTimerTest(HybridBCase): def _lease_exec(self): return self.make_exec(env_extra={ "CHV_RESIDENT_IDLE_SUSPEND_SECONDS": "60", "CHV_RESIDENT_IDLE_DEMOTE_SECONDS": "120"}) def test_sweep_suspends_idle_active_already_paused(self): ex = self._lease_exec() now = 1800000000.0 vm = self._write_resident(ex, "t1", "resident-active", "Paid", now - 61) self._live_vm(ex, vm) self.vm_states[vm] = "Paused" out = ex.sweep_resident_leases(now=now) self.assertEqual(out["suspended"], ["t1"]) self.assertEqual(out["demoted"], []) record = read_persistent_vm(ex.state_dir, "t1") self.assertEqual(record["state"], "resident-paused") self.assertEqual(record["tier"], "Paid") self.assertIn("vm_resident_suspended", self._event_names()) detail = [d for e, d, _k in self.events if e == "vm_resident_suspended"][0] self.assertIn("host_id=host-test-1", detail) self.assertIn("tenant=t1", detail) # VM 本已 Paused:sweep 不应再发 pause。 self.assertFalse(any(c[-1] == "pause" for c in self.calls if c and "ch-remote" in c[0])) def test_sweep_suspends_running_vm_calls_pause(self): ex = self._lease_exec() now = 1800000000.0 vm = self._write_resident(ex, "t1", "resident-active", "Paid", now - 61) self._live_vm(ex, vm) self.vm_states[vm] = "Running" out = ex.sweep_resident_leases(now=now) self.assertEqual(out["suspended"], ["t1"]) self.assertTrue(any(c[-1] == "pause" for c in self.calls if c and "ch-remote" in c[0])) self.assertEqual( read_persistent_vm(ex.state_dir, "t1")["state"], "resident-paused") def test_sweep_no_suspend_before_threshold(self): ex = self._lease_exec() now = 1800000000.0 self._write_resident(ex, "t1", "resident-active", "Paid", now - 30) out = ex.sweep_resident_leases(now=now) self.assertEqual(out, {"suspended": [], "demoted": [], "aborted": []}) self.assertEqual( read_persistent_vm(ex.state_dir, "t1")["state"], "resident-active") def test_sweep_demotes_after_24h(self): ex = self._lease_exec() now = 1800000000.0 vm = self._write_resident(ex, "t1", "resident-paused", "Paid", now - 61 - 121) self._live_vm(ex, vm) self.vm_states[vm] = "Paused" # 用户数据盘与任务盘:降级后须保留(快照输入 + 用户数据)。 vm_dir = os.path.join(ex.state_dir, vm) os.makedirs(vm_dir, exist_ok=True) with open(os.path.join(vm_dir, "data.raw"), "wb") as fh: fh.write(b"D" * 10) with open(os.path.join(vm_dir, "disk.raw"), "wb") as fh: fh.write(b"K" * 10) out = ex.sweep_resident_leases(now=now) self.assertEqual(out["demoted"], ["t1"]) record = read_persistent_vm(ex.state_dir, "t1") self.assertEqual(record["state"], "demoted") self.assertEqual(record["last_active_at"], now - 61 - 121) self.assertIn("snapshot_url", record) self.assertIn("vm_resident_demoted", self._event_names()) detail = [d for e, d, _k in self.events if e == "vm_resident_demoted"][0] self.assertIn("host_id=host-test-1", detail) self.assertIn("tenant=t1", detail) self.assertTrue(os.path.exists( os.path.join(vm_dir, "data.raw"))) self.assertTrue(os.path.exists( os.path.join(vm_dir, "disk.raw"))) # 槽位已释放:可再 promote 别的租户。 self.assertEqual(ex._resident_count(), 0) def test_demote_sync_failure_aborts(self): ex = self._lease_exec() now = 1800000000.0 vm = self._write_resident(ex, "t1", "resident-paused", "Paid", now - 61 - 121) self._live_vm(ex, vm) self.vm_states[vm] = "Paused" out = ex.demote_resident( "t1", sync_fn=lambda tenant, v: False, now=now) self.assertFalse(out["demoted"]) self.assertEqual(out["reason"], "sync_failed") record = read_persistent_vm(ex.state_dir, "t1") self.assertEqual(record["state"], "resident-paused") self.assertNotIn("snapshot_url", record) # VM 保留:socket 还在,未销毁。 self.assertTrue(os.path.exists( os.path.join(ex.state_dir, vm + ".sock"))) self.assertIn("vm_resident_demote_aborted", self._event_names()) def test_demote_snapshot_failure_aborts(self): ex = self._lease_exec() now = 1800000000.0 vm = self._write_resident(ex, "t1", "resident-paused", "Paid", now - 61 - 121) self._live_vm(ex, vm) self.vm_states[vm] = "Paused" with mock.patch.object( ex, "snapshot_vm", side_effect=ChvFailure("snapshot_failed", "boom")): out = ex.demote_resident("t1", now=now) self.assertFalse(out["demoted"]) self.assertEqual(out["reason"], "snapshot_failed") self.assertEqual( read_persistent_vm(ex.state_dir, "t1")["state"], "resident-paused") def test_demote_not_demotable_states(self): ex = self._lease_exec() now = 1800000000.0 self._write_resident(ex, "t1", "resident-active", "Paid", now - 100000) out = ex.demote_resident("t1", now=now) self.assertFalse(out["demoted"]) self.assertEqual(out["reason"], "not_demotable") out = ex.demote_resident("ghost", now=now) self.assertFalse(out["demoted"]) self.assertEqual(out["reason"], "not_demotable") def test_sweep_demote_aborted_on_sync_hook_failure(self): ex = self._lease_exec() ex.resident_sync_hook = lambda tenant, vm: False now = 1800000000.0 vm = self._write_resident(ex, "t1", "resident-paused", "Paid", now - 61 - 121) self._live_vm(ex, vm) self.vm_states[vm] = "Paused" out = ex.sweep_resident_leases(now=now) self.assertEqual(out["demoted"], []) self.assertEqual(out["aborted"], ["t1"]) self.assertEqual( read_persistent_vm(ex.state_dir, "t1")["state"], "resident-paused") # -- 排队超时拒绝(第 7 项)-------------------------------------------- class QueueTest(HybridBCase): def _queue_exec(self): return self.make_exec(env_extra={ "CHV_MAX_VMS": "1", "CHV_QUEUE_TIMEOUT_SECONDS": "10", "CHV_QUEUE_POLL_SECONDS": "2"}) def test_wait_for_capacity_timeout_rejects(self): ex = self._queue_exec() occupant = FakeProc(alive=True) ex._vms["chv-other"] = occupant with self.assertRaises(ChvFailure) as ctx: ex._wait_for_capacity(self._task(), exclude="chv-abc123") self.assertEqual(ctx.exception.kind, "host_capacity_exceeded") # 轮询 10/2 = 5 次 sleeper 调用后才拒绝。 self.assertEqual(len(self.sleeps), 5) # 不抢占:占位 VM 仍活着;队列计数已归零。 self.assertIsNone(occupant.poll()) self.assertEqual(ex.queue_depth(), 0) def test_wait_for_capacity_free_passes_immediately(self): ex = self._queue_exec() ex._wait_for_capacity(self._task(), exclude="chv-abc123") self.assertEqual(self.sleeps, []) def test_run_with_vm_queues_before_wake(self): ex = self.make_exec(env_extra={ "CHV_MAX_VMS": "2", "CHV_QUEUE_TIMEOUT_SECONDS": "10", "CHV_QUEUE_POLL_SECONDS": "2"}) ex._vms["chv-other1"] = FakeProc(alive=True) ex._vms["chv-other2"] = FakeProc(alive=True) # 常驻租户排队:档位与租约原样保留,不升降档。 # (max_vms=2 → 常驻上限 1,promote 恰好占满 1 槽。) ex.promote_to_resident("tq", "Paid") wake_calls = [] ex.wake_vm = lambda *a, **k: wake_calls.append(a) with self.assertRaises(ChvFailure) as ctx: ex._run_with_vm(self._task(project="tq"), ["dispatch", "--name", "tq", "--task", "hi"]) self.assertEqual(ctx.exception.kind, "host_capacity_exceeded") self.assertEqual(wake_calls, []) record = read_persistent_vm(ex.state_dir, "tq") self.assertEqual(record["tier"], "Paid") self.assertEqual(record["state"], "resident-active") def test_wait_for_resident_slot_timeout(self): ex = self.make_exec(env_extra={ "CHV_MAX_VMS": "4", "CHV_QUEUE_TIMEOUT_SECONDS": "10", "CHV_QUEUE_POLL_SECONDS": "2"}) ex.promote_to_resident("t1", "Paid") ex.promote_to_resident("t2", "Heavy") with self.assertRaises(ChvFailure) as ctx: ex._wait_for_resident_slot("t3") self.assertEqual(ctx.exception.kind, "host_capacity_exceeded") # 不抢占、不挪池:前两个常驻原样。 self.assertEqual(ex._resident_count(), 2) self.assertEqual( read_persistent_vm(ex.state_dir, "t1")["state"], "resident-active") def test_wait_for_resident_slot_free_passes(self): ex = self.make_exec(env_extra={"CHV_MAX_VMS": "4"}) ex.promote_to_resident("t1", "Paid") ex._wait_for_resident_slot("t2") self.assertEqual(self.sleeps, []) # -- 升级提示点去重(第 8 项)------------------------------------------ class UpgradeHintTest(HybridBCase): def test_hint_at_100_tasks_in_30d_and_dedup(self): ex = self.make_exec() now = 1800000000.0 hinted_at = None for i in range(100): out = ex.record_pool_task("tfree", tier="Free", now=now - 1000 + i * 10) if out["hinted"]: hinted_at = i self.assertEqual(hinted_at, 99) self.assertEqual( self._event_names().count("resident_upgrade_hint"), 1) detail = [d for e, d, _k in self.events if e == "resident_upgrade_hint"][0] self.assertIn("host_id=host-test-1", detail) self.assertIn("tenant=tfree", detail) self.assertIn("tier=Free", detail) self.assertIn("30d_tasks=100", detail) # 第 101 次:一周内同一租户不再记第二次。 out = ex.record_pool_task("tfree", tier="Free", now=now + 10) self.assertFalse(out["hinted"]) self.assertEqual( self._event_names().count("resident_upgrade_hint"), 1) # 一周后:可再记一次。 out = ex.record_pool_task("tfree", tier="Free", now=now + 8 * 86400) self.assertTrue(out["hinted"]) self.assertEqual( self._event_names().count("resident_upgrade_hint"), 2) def test_hint_on_7day_streak(self): ex = self.make_exec() now = 1800000000.0 today = int(now // 86400) hinted = False for day_offset in range(6, -1, -1): day_start = (today - day_offset) * 86400 + 3600 for j in range(3): out = ex.record_pool_task( "tstreak", tier="Free", now=day_start + j * 60) hinted = hinted or out["hinted"] self.assertTrue(hinted) self.assertEqual( self._event_names().count("resident_upgrade_hint"), 1) detail = [d for e, d, _k in self.events if e == "resident_upgrade_hint"][0] self.assertIn("7d_streak_3_per_day", detail) def test_no_hint_below_streak(self): ex = self.make_exec() now = 1800000000.0 today = int(now // 86400) for day_offset in range(5, -1, -1): day_start = (today - day_offset) * 86400 + 3600 for j in range(3): out = ex.record_pool_task( "tshort", tier="Free", now=day_start + j * 60) self.assertFalse(out["hinted"]) self.assertEqual(self._event_names(), []) def test_paid_tier_not_recorded(self): ex = self.make_exec() out = ex.record_pool_task("tpaid", tier="Paid", now=1800000000.0) self.assertFalse(out["recorded"]) self.assertFalse(out["hinted"]) self.assertFalse(os.path.exists( ex._pool_task_file("tpaid"))) # -- 网关分流(第 9 项)------------------------------------------------- class GatewaySplitTest(HybridBCase): def test_gateway_allowed_resident_paid(self): ex = self.make_exec() ex.promote_to_resident("t1", "Paid") task = self._task(project="t1") self.assertTrue(ex.gateway_allowed_for_task( task, model_provider="sub2api")) self.assertTrue(ex.gateway_allowed_for_task( dict(task, _gateway_provider="sub2api"))) def test_gateway_allowed_hibernated_resident(self): ex = self.make_exec() now = 1800000000.0 self._write_resident(ex, "t1", "hibernated", "Paid", now - 10) self.assertTrue(ex.gateway_allowed_for_task( self._task(project="t1"), model_provider="sub2api")) def test_gateway_denied_pool_tenants(self): ex = self.make_exec() # 无登记:池。 self.assertFalse(ex.gateway_allowed_for_task( self._task(project="tpool"), model_provider="sub2api")) # 常驻态但 tier 是 Free:无常驻资格,按池算。 now = 1800000000.0 self._write_resident(ex, "tfree", "resident-active", "Free", now - 10) self.assertFalse(ex.gateway_allowed_for_task( self._task(project="tfree"), model_provider="sub2api")) # demoted:租约已收回,按池算。 self._write_resident(ex, "tdem", "demoted", "Paid", now - 10) self.assertFalse(ex.gateway_allowed_for_task( self._task(project="tdem"), model_provider="sub2api")) # legacy paused 登记:不是常驻态,按池算。 vm = ex.persistent_vm_name("tleg") write_persistent_vm(ex.state_dir, "tleg", { "vm": vm, "tenant_id": "tleg", "state": "paused", "origin": "task", "gitea_synced": True, "updated_at": now}) self.assertFalse(ex.gateway_allowed_for_task( self._task(project="tleg"), model_provider="sub2api")) def test_gateway_check_non_gateway_not_blocked(self): ex = self.make_exec() # 非网关任务不由本门拦(model_provider 不是网关)。 self.assertTrue(ex.gateway_allowed_for_task( self._task(project="tpool"), model_provider=None)) self.assertTrue(ex.gateway_allowed_for_task( self._task(project="tpool"), model_provider="xcmapi")) def test_run_with_vm_gateway_pool_denied_before_wake(self): ex = self.make_exec() wake_calls = [] ex.wake_vm = lambda *a, **k: wake_calls.append(a) with self.assertRaises(ChvFailure) as ctx: ex._run_with_vm(self._task(project="tpool"), ["dispatch", "--name", "tpool", "--task", "hi"], model_provider="sub2api") self.assertEqual(ctx.exception.kind, "gateway_pool_denied") self.assertEqual(wake_calls, []) def test_run_with_vm_gateway_resident_passes_gate(self): ex = self.make_exec() ex.promote_to_resident("t1", "Paid") # 常驻租户的网关任务过门不抛错(全程 dispatch 由既有 # 测试覆盖,这里只断言门本身放行)。 ex._check_gateway_vm_class( self._task(project="t1"), model_provider="sub2api") if __name__ == "__main__": unittest.main()