Files
codex-app-server/cloud/tests/test_chv_hybrid_b.py
T
host 667a427349 feat(chv): 混合模式 B 轨——租约 + 排队 + 升级提示 + 网关门
- 登记扩展 tier/state/last_active_at/lease_expires_at;认领三重核对
  原样保留,新增 tier 校验(无 tier 行 = 无常驻资格,fail-closed;
  与 P11 用量门不对称已写进注释)
- 租约计时器:闲置 30 分钟挂起、再 24 小时降级;阈值经环境变量注入,
  生产默认值不变;worker 心跳轮询驱动 sweep_resident_leases;
  降级同步失败即中止、VM 保留
- 排队:300 秒超时后 host_capacity_exceeded 明确拒绝;不插队、
  不抢占常驻槽位、不升降档;常驻任务仍等常驻槽位;队列深度供 L1 读
- 升级提示点 resident_upgrade_hint:30 天 ≥100 次或连续 7 天每天 ≥3 次,
  一周内同一租户最多一次;频率只用于提示,绝不改路由
- 网关分流:常驻 VM 可接网关、池 VM 以 gateway_pool_denied 拒绝
  (worker 侧守卫自 P1-1 起已放行 CHV,分流 enforcement 落执行器)
2026-11-08 20:49:10 +00:00

711 lines
30 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 混合模式 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()