Files
codex-app-server/cloud/tests/test_chv_muse_p9.py
T
host c06ba50983 feat-chv-muse-p9: 推理与遥测受限通道(Sentinel 预审 + 遥测最小化 + 训练退出开关)
- 受限推理通道:cloud/chv/inference_channel.py(origin 解析、
  allowlist、上限、Sentinel 预审),执行器 _run_with_vm 起 VM 前
  必过 _inference_gate(allow 才继续,deny/ask 审计 + fail-closed,
  任务级 extra_env 合并进预审);host.py 第二道防线(_server_command
  的 origin 结构校验 + Server.request 超时/重试走 env)。
  origin 规则:https 公网网关(只 443);http 只接受本地中继
  (字面环回/私网 IP,如 http://172.17.0.1:28084,端口保留,
  无 DNS、无重绑定,allowlist 与单次能力照常);未知 provider
  标签走默认网关(与 _server_command 一致)。
- sentinel.py:从 P5(25bcd35)取的超集版本 + 推理连接器条目
  (POST /v1/responses、GET /v1/models;scheme/端口只 https/443,
  本地中继不走 evaluate,不把 http 加进表)。
- 遥测最小化:cloud/chv/telemetry.py(字段白名单、确定性采样、
  落盘失败不影响任务、token 未回传记 null)+ 训练退出开关
  (默认开,本项目决定;关后轨迹不再产生;损坏 fail-closed;变更审计)。
- 访问边界:docs/chv-access-boundary.md(运营访问口径 + 审计钩子,
  不冒充技术不可访问;Confidential VM 明确未做)。
- 测试:cloud/tests/test_chv_muse_p9.py 51 组全过;5 个旧文件
  的 make_exec 配假 origin + 假解析器(新 fail-closed 门要求的
  测试配置);全量 cloud 套件 1918 OK(skipped=8)。
  详见 docs/chv-muse-p9-gap.md(含自定清单与真机待验证清单)。
2026-11-06 23:03:06 +00:00

571 lines
24 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.
"""对齐 Muse P9:推理与遥测受限通道。
全部用标准库与 mock,不启动真实 VM、不联网、不碰 WSL/真机;
解析器一律用假解析器(公网 IP / 私网 IP 两档)。
"""
import importlib.util
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
try:
from cloud.chv.executor import CloudHypervisorExecutor
from cloud.chv.inference_channel import (
InferenceLimits,
check_inference_call,
inference_allow_hosts,
inference_limits,
resolve_inference_origin,
)
from cloud.chv.sentinel import SentinelPolicy
from cloud.chv.telemetry import (
build_task_telemetry,
build_trajectory_event,
emit_telemetry,
sanitize_telemetry_event,
sampled,
set_training_enabled,
telemetry_sample_rate,
training_enabled,
)
from cloud.sandbox import ChvFailure
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.chv.executor import CloudHypervisorExecutor
from cloud.chv.inference_channel import (
InferenceLimits,
check_inference_call,
inference_allow_hosts,
inference_limits,
resolve_inference_origin,
)
from cloud.chv.sentinel import SentinelPolicy
from cloud.chv.telemetry import (
build_task_telemetry,
build_trajectory_event,
emit_telemetry,
sanitize_telemetry_event,
sampled,
set_training_enabled,
telemetry_sample_rate,
training_enabled,
)
from cloud.sandbox import ChvFailure
def _public_resolver(host, port):
"""假解析器:公网 IP。"""
return ["93.184.216.34"]
def _private_resolver(host, port):
"""假解析器:私网 IP(SSRF 场景)。"""
return ["10.0.0.5"]
def _load_host_module():
"""以独立模块名加载仓库根的 host.py(只用标准库)。"""
path = Path(__file__).resolve().parents[2] / "host.py"
spec = importlib.util.spec_from_file_location("host_under_test", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class OriginTest(unittest.TestCase):
def test_valid_sub2api_origin(self):
env = {"SUB2API_BASE_URL": "https://xgapi.lol/"}
self.assertEqual(
resolve_inference_origin("sub2api", env), "https://xgapi.lol")
def test_missing_origin_returns_empty(self):
self.assertEqual(resolve_inference_origin("sub2api", {}), "")
self.assertEqual(resolve_inference_origin(None, {}), "")
def test_http_origin_rejected(self):
env = {"SUB2API_BASE_URL": "http://xgapi.lol"}
with self.assertRaises(ValueError):
resolve_inference_origin("sub2api", env)
def test_http_public_ip_rejected(self):
env = {"SUB2API_BASE_URL": "http://8.8.8.8:28084"}
with self.assertRaises(ValueError):
resolve_inference_origin("sub2api", env)
def test_http_hostname_rejected(self):
env = {"SUB2API_BASE_URL": "http://relay.local:28084"}
with self.assertRaises(ValueError):
resolve_inference_origin("sub2api", env)
def test_http_local_relay_allowed(self):
# 文档形状:本地 sub2api 中继走 Docker 桥接 IP + http,
# 端口保留;环回同款。
env = {"SUB2API_BASE_URL": "http://172.17.0.1:28084/"}
self.assertEqual(
resolve_inference_origin("sub2api", env),
"http://172.17.0.1:28084")
env2 = {"SUB2API_BASE_URL": "http://127.0.0.1:28084"}
self.assertEqual(
resolve_inference_origin("sub2api", env2),
"http://127.0.0.1:28084")
def test_userinfo_in_origin_rejected(self):
env = {"SUB2API_BASE_URL": "https://user:pass@xgapi.lol"}
with self.assertRaises(ValueError):
resolve_inference_origin("sub2api", env)
def test_non_443_port_rejected(self):
env = {"SUB2API_BASE_URL": "https://xgapi.lol:8443"}
with self.assertRaises(ValueError):
resolve_inference_origin("sub2api", env)
def test_unknown_provider_falls_back_to_default_origin(self):
# 未知标签与 host.py `_server_command` 一致走默认网关;
# 未配 CHV_INFERENCE_ORIGIN 时返回 ""(调用方按 ask 处理)。
env = {"CHV_INFERENCE_ORIGIN": "https://xgapi.lol"}
self.assertEqual(
resolve_inference_origin("openai", env), "https://xgapi.lol")
self.assertEqual(resolve_inference_origin("openai", {}), "")
def test_xcmapi_origin_from_explicit_env(self):
env = {"CHV_INFERENCE_ORIGIN": "https://xgapi.lol"}
self.assertEqual(
resolve_inference_origin(None, env), "https://xgapi.lol")
class AllowlistTest(unittest.TestCase):
def test_explicit_allowlist_wins(self):
env = {"SUB2API_BASE_URL": "https://xgapi.lol",
"CHV_INFERENCE_ALLOW_HOSTS": "a.example, B.example "}
hosts = inference_allow_hosts("sub2api", env)
self.assertEqual(hosts, frozenset({"a.example", "b.example"}))
def test_default_allowlist_is_origin_host(self):
env = {"SUB2API_BASE_URL": "https://xgapi.lol"}
self.assertEqual(inference_allow_hosts("sub2api", env),
frozenset({"xgapi.lol"}))
def test_no_origin_means_empty_allowlist(self):
self.assertEqual(inference_allow_hosts("sub2api", {}), frozenset())
class LimitsTest(unittest.TestCase):
def test_defaults(self):
self.assertEqual(inference_limits({}),
InferenceLimits(timeout_seconds=120,
max_retries=1))
def test_env_override(self):
env = {"CHV_INFERENCE_TIMEOUT": "60",
"CHV_INFERENCE_MAX_RETRIES": "3"}
self.assertEqual(inference_limits(env),
InferenceLimits(timeout_seconds=60, max_retries=3))
def test_invalid_timeout_rejected(self):
for bad in ("abc", "5", "601", "-1"):
with self.assertRaises(ValueError):
inference_limits({"CHV_INFERENCE_TIMEOUT": bad})
def test_invalid_retries_rejected(self):
for bad in ("x", "-1", "6"):
with self.assertRaises(ValueError):
inference_limits({"CHV_INFERENCE_MAX_RETRIES": bad})
class PreflightTest(unittest.TestCase):
def setUp(self):
self.env = {"SUB2API_BASE_URL": "https://xgapi.lol",
"CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"}
def test_allow_with_public_ip(self):
check = check_inference_call(
"sub2api", self.env, resolve=_public_resolver)
self.assertEqual(check.decision.verdict, "allow")
self.assertIsNotNone(check.decision.capability)
self.assertTrue(check.decision.capability.matches(check.request))
self.assertEqual(check.request.host, "xgapi.lol")
self.assertEqual(check.request.path, "/v1/responses")
def test_capability_single_use_300s(self):
check = check_inference_call(
"sub2api", self.env, resolve=_public_resolver, now=1000.0)
cap = check.decision.capability
self.assertTrue(cap.use(check.request, now=1000.0))
self.assertFalse(cap.use(check.request, now=1000.0))
self.assertFalse(cap.matches(check.request, now=2000.0))
def test_local_relay_allow_without_dns(self):
# 本地中继是字面私网 IP:不查 DNS(解析器被调即错),
# allowlist 默认即 origin 主机,直接 allow + 单次能力。
def _must_not_resolve(host, port):
raise AssertionError("local relay must not resolve")
env = {"SUB2API_BASE_URL": "http://172.17.0.1:28084"}
check = check_inference_call(
"sub2api", env, resolve=_must_not_resolve)
self.assertEqual(check.decision.verdict, "allow")
self.assertIn("local_relay", check.decision.reasons)
self.assertIsNotNone(check.decision.capability)
self.assertEqual(check.request.host, "172.17.0.1")
self.assertEqual(check.request.port, 28084)
self.assertEqual(check.request.scheme, "http")
def test_local_relay_ask_when_not_allowlisted(self):
env = {"SUB2API_BASE_URL": "http://172.17.0.1:28084",
"CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"}
check = check_inference_call(
"sub2api", env, resolve=_public_resolver)
self.assertEqual(check.decision.verdict, "ask")
def test_deny_on_private_ip(self):
check = check_inference_call(
"sub2api", self.env, resolve=_private_resolver)
self.assertEqual(check.decision.verdict, "deny")
self.assertIn("ssrf_private_ip", check.decision.reasons[0])
def test_ask_on_non_allowlisted_host(self):
# 显式 allowlist 已配、origin 主机不在其中 → ask。
# (未配 allowlist 时默认即 origin 主机自身,不会 ask——
# 见 test_default_allowlist_is_origin_host。)
env = {"SUB2API_BASE_URL": "https://unknown.example",
"CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"}
check = check_inference_call(
"sub2api", env, resolve=_public_resolver)
self.assertEqual(check.decision.verdict, "ask")
def test_ask_when_origin_not_configured(self):
check = check_inference_call(
None, {}, resolve=_public_resolver)
self.assertEqual(check.decision.verdict, "ask")
self.assertIn("inference_origin_not_configured",
check.decision.reasons)
def test_xcmapi_origin_allow(self):
env = {"CHV_INFERENCE_ORIGIN": "https://xgapi.lol",
"CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"}
check = check_inference_call(
None, env, resolve=_public_resolver)
self.assertEqual(check.decision.verdict, "allow")
def test_unknown_provider_falls_back_to_default_origin(self):
# 未知标签不 deny:与 _server_command 一致走默认网关;
# 未配 CHV_INFERENCE_ORIGIN 时仍是 ask(fail-closed)。
env = {"CHV_INFERENCE_ORIGIN": "https://xgapi.lol",
"CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"}
check = check_inference_call(
"prov-x", env, resolve=_public_resolver)
self.assertEqual(check.decision.verdict, "allow")
check2 = check_inference_call(
"prov-x", {}, resolve=_public_resolver)
self.assertEqual(check2.decision.verdict, "ask")
def test_deny_path_out_of_scope(self):
policy = SentinelPolicy(
allow_hosts={"xgapi.lol"},
connector_paths={"inference": (("POST", "prefix",
"/v1/responses"),)})
from cloud.chv.sentinel import EgressRequest
request = EgressRequest("inference", "GET", "https",
"xgapi.lol", 443, "/v1/admin")
decision = policy.evaluate(request, resolve=_public_resolver)
self.assertEqual(decision.verdict, "deny")
def test_audit_record_has_no_values(self):
check = check_inference_call(
"sub2api", self.env, resolve=_public_resolver)
record = check.decision.audit(check.request)
blob = json.dumps(record)
self.assertNotIn("xgapi.lol".replace("xgapi", "SECRET"), blob)
self.assertEqual(record["host"], "xgapi.lol")
self.assertEqual(record["verdict"], "allow")
class FakeResult:
def __init__(self, returncode=0, stdout="", stderr=""):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
def _write(path, content=b"x"):
with open(path, "wb") as fh:
fh.write(content)
os.chmod(path, 0o755)
return path
class GateTest(unittest.TestCase):
"""执行器预审:deny/ask 时不起 VM(wake 未被调用);allow 才继续。"""
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.wake_calls = []
self.audit = []
def tearDown(self):
self.tmp.cleanup()
def _make_exec(self, env_extra):
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
"CHV_HOST_KEY_MODE": "keyscan",
"CHV_SSH_FORWARD": "1"}
env.update(env_extra)
ex = CloudHypervisorExecutor(
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=lambda cmd, timeout=None: FakeResult(0, "", ""),
launcher=lambda cmd: FakeResult(0, "", ""),
api_client=lambda *a, **k: FakeResult(0, "", ""),
sleeper=lambda s: None, env=env)
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
def test_deny_blocks_wake(self):
ex = self._make_exec(
{"SUB2API_BASE_URL": "https://xgapi.lol",
"CHV_INFERENCE_ALLOW_HOSTS": "xgapi.lol"})
ex.sentinel_resolver = _private_resolver
ex.wake_vm = mock.Mock(side_effect=AssertionError("must not wake"))
task = {"id": "t1", "prompt": "hi"}
with self.assertRaises(ChvFailure) as ctx:
ex.dispatch_task(task, model_provider="sub2api")
self.assertEqual(ctx.exception.kind, "inference_egress_denied")
ex.wake_vm.assert_not_called()
kinds = [e[0] for e in self.audit]
self.assertIn("inference_egress_deny", kinds)
def test_ask_blocks_wake_when_origin_missing(self):
ex = self._make_exec({})
ex.sentinel_resolver = _public_resolver
ex.wake_vm = mock.Mock(side_effect=AssertionError("must not wake"))
task = {"id": "t2", "prompt": "hi"}
with self.assertRaises(ChvFailure) as ctx:
ex.dispatch_task(task, model_provider=None)
self.assertEqual(ctx.exception.kind, "inference_egress_denied")
ex.wake_vm.assert_not_called()
def test_extra_env_origin_is_honored(self):
# 网关配置随任务来(worker 的 extra_env),执行器
# 进程 env 里没有 origin 也要放行。
ex = self._make_exec({})
ex.sentinel_resolver = _public_resolver
ex.wake_vm = mock.Mock()
task = {"id": "t3", "prompt": "hi"}
with mock.patch.object(ex, "_run_guest",
return_value={"status": "completed"}):
with mock.patch.object(ex, "shutdown_vm"):
ex.dispatch_task(
task, model_provider="sub2api",
extra_env={"SUB2API_BASE_URL": "https://xgapi.lol"})
ex.wake_vm.assert_called_once()
def test_bad_origin_becomes_deny_not_value_error(self):
ex = self._make_exec({})
ex.sentinel_resolver = _public_resolver
ex.wake_vm = mock.Mock(side_effect=AssertionError("must not wake"))
task = {"id": "t4", "prompt": "hi"}
with self.assertRaises(ChvFailure) as ctx:
ex.dispatch_task(
task, model_provider="sub2api",
extra_env={"SUB2API_BASE_URL": "http://xgapi.lol"})
self.assertEqual(ctx.exception.kind, "inference_egress_denied")
ex.wake_vm.assert_not_called()
kinds = [e[0] for e in self.audit]
self.assertIn("inference_egress_deny", kinds)
class TelemetryTest(unittest.TestCase):
def test_allowlisted_fields_only(self):
event = build_task_telemetry(
task_id="t1", vm_id="vm1", provider="sub2api",
model="gpt-6.1-terra", status="completed",
started_at=1000.0, ended_at=1005.0, duration_ms=5000,
tokens={"input_tokens": 10, "output_tokens": 5,
"total_tokens": 15},
turns=2, tool_calls=3, env={"CHV_TELEMETRY_SAMPLE_RATE": "1"})
self.assertIsNotNone(event)
self.assertEqual(event["tokens_in"], 10)
self.assertEqual(event["tokens_total"], 15)
self.assertEqual(event["turns"], 2)
def test_secret_and_text_keys_dropped(self):
event = {"task_id": "t1", "vm_id": "v", "provider": "sub2api",
"model": "m", "status": "completed",
"started_at": 1.0, "ended_at": 2.0, "duration_ms": 1,
"prompt": "do evil", "content": "secret file",
"api_key": "sk-123", "user_email": "a@b.c",
"extra_blob": {"nested": "data"}}
clean = sanitize_telemetry_event(event)
for bad in ("prompt", "content", "api_key", "user_email",
"extra_blob"):
self.assertNotIn(bad, clean)
self.assertEqual(clean["task_id"], "t1")
def test_tokens_none_when_not_reported(self):
event = build_task_telemetry(
task_id="t1", vm_id="vm1", provider="sub2api", model=None,
status="failed", started_at=1.0, ended_at=2.0, duration_ms=1,
env={"CHV_TELEMETRY_SAMPLE_RATE": "1"})
self.assertIsNone(event["tokens_in"])
self.assertIsNone(event["tokens_out"])
self.assertIsNone(event["tokens_total"])
def test_sampling_deterministic(self):
self.assertEqual(sampled("task-abc", 1.0), True)
self.assertEqual(sampled("task-abc", 0.0), False)
self.assertEqual(sampled("task-abc", 0.5),
sampled("task-abc", 0.5))
event = build_task_telemetry(
task_id="t1", vm_id="v", provider="sub2api", model=None,
status="completed", started_at=1.0, ended_at=2.0,
duration_ms=1, env={"CHV_TELEMETRY_SAMPLE_RATE": "0"})
self.assertIsNone(event)
def test_invalid_sample_rate_rejected(self):
with self.assertRaises(ValueError):
telemetry_sample_rate({"CHV_TELEMETRY_SAMPLE_RATE": "2"})
with self.assertRaises(ValueError):
telemetry_sample_rate({"CHV_TELEMETRY_SAMPLE_RATE": "x"})
def test_emit_never_raises(self):
self.assertFalse(emit_telemetry("/nonexistent/dir/xyz",
{"task_id": "t1"}))
self.assertFalse(emit_telemetry("", {"task_id": "t1"}))
def test_emit_writes_jsonl(self):
with tempfile.TemporaryDirectory() as d:
event = {"task_id": "t9", "vm_id": "v", "status": "completed"}
self.assertTrue(emit_telemetry(d, event))
with open(os.path.join(d, "telemetry.jsonl")) as fh:
row = json.loads(fh.read().strip())
self.assertEqual(row["task_id"], "t9")
self.assertNotIn("prompt", row)
class TrainingOptTest(unittest.TestCase):
def test_default_enabled(self):
with tempfile.TemporaryDirectory() as d:
self.assertTrue(training_enabled(d))
def test_set_off_persists_and_blocks_trajectory(self):
with tempfile.TemporaryDirectory() as d:
self.assertTrue(set_training_enabled(d, False))
self.assertFalse(training_enabled(d))
event = build_trajectory_event(
"t1", 3, ["read", "write"], 100, d)
self.assertIsNone(event)
# 任务级遥测不受开关影响
task_event = build_task_telemetry(
"t1", "v", "sub2api", None, "completed",
1.0, 2.0, 1, env={"CHV_TELEMETRY_SAMPLE_RATE": "1"})
self.assertIsNotNone(task_event)
def test_trajectory_has_metadata_only(self):
with tempfile.TemporaryDirectory() as d:
event = build_trajectory_event(
"t1", 2, ["read", "exec"], 50, d)
self.assertIsNotNone(event)
self.assertEqual(event["tools"], ["read", "exec"])
self.assertNotIn("prompt", event)
self.assertNotIn("content", event)
def test_corrupt_state_fails_closed(self):
with tempfile.TemporaryDirectory() as d:
with open(os.path.join(d, "training-opt.json"), "w") as fh:
fh.write("{not json")
self.assertFalse(training_enabled(d))
def test_non_bool_rejected(self):
with tempfile.TemporaryDirectory() as d:
with self.assertRaises(TypeError):
set_training_enabled(d, "off")
def test_switch_change_is_audited(self):
with tempfile.TemporaryDirectory() as d:
set_training_enabled(d, False, actor="tester")
audit_path = os.path.join(d, "training-opt-audit.jsonl")
with open(audit_path) as fh:
row = json.loads(fh.read().strip())
self.assertEqual(row["event"], "training_opt_changed")
self.assertFalse(row["enabled"])
self.assertEqual(row["actor"], "tester")
class HostPyTest(unittest.TestCase):
def setUp(self):
self.host = _load_host_module()
def test_server_command_valid_origin(self):
with mock.patch.dict(os.environ,
{"SUB2API_BASE_URL": "https://xgapi.lol"}):
cmd = self.host._server_command("codex", provider="sub2api")
self.assertIn("app-server", cmd)
self.assertTrue(any("xgapi.lol/v1" in c for c in cmd))
def test_server_command_http_origin_rejected(self):
with mock.patch.dict(os.environ,
{"SUB2API_BASE_URL": "http://xgapi.lol"}):
with self.assertRaises(ValueError):
self.host._server_command("codex", provider="sub2api")
def test_server_command_local_relay_allowed(self):
# 文档形状的本地中继(http + 桥接 IP)必须通过第二道防线。
with mock.patch.dict(os.environ,
{"SUB2API_BASE_URL":
"http://172.17.0.1:28084"}):
cmd = self.host._server_command("codex", provider="sub2api")
self.assertTrue(any("172.17.0.1:28084/v1" in c for c in cmd))
def test_server_command_allowlist_mismatch_rejected(self):
with mock.patch.dict(os.environ,
{"SUB2API_BASE_URL": "https://xgapi.lol",
"CHV_INFERENCE_ALLOW_HOSTS": "other.example"}):
with self.assertRaises(ValueError):
self.host._server_command("codex", provider="sub2api")
def test_request_timeout_default_and_env(self):
self.assertEqual(
self.host._inference_request_timeout(None), 120)
self.assertEqual(
self.host._inference_request_timeout(60), 60)
with mock.patch.dict(os.environ,
{"CHV_INFERENCE_TIMEOUT": "30"}):
self.assertEqual(
self.host._inference_request_timeout(None), 30)
with mock.patch.dict(os.environ,
{"CHV_INFERENCE_TIMEOUT": "5"}):
with self.assertRaises(ValueError):
self.host._inference_request_timeout(None)
def test_request_retries_default_and_env(self):
self.assertEqual(self.host._inference_max_retries(), 1)
with mock.patch.dict(os.environ,
{"CHV_INFERENCE_MAX_RETRIES": "0"}):
self.assertEqual(self.host._inference_max_retries(), 0)
with mock.patch.dict(os.environ,
{"CHV_INFERENCE_MAX_RETRIES": "9"}):
with self.assertRaises(ValueError):
self.host._inference_max_retries()
if __name__ == "__main__":
unittest.main()