Files
codex-app-server/cloud/tests/test_audit_batch4.py
T

674 lines
29 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.
"""审计修复批 4 测试:I-6、I-7、I-8、I-10、I-11、I-13。
只用注入的假 runner 与 init_db 的本地库,不跑真实模型回合、
不联网、不登录任何服务;env 里的值只是占位用的假名。
行为变更锁定(本批有意改语义,测试即契约):
- I-6:策略写入路径(能力目录键、档模型、fallback_models)
含空白的一律丢弃;影子行 chosen(行尾字段)含空白可解析;
persisted 行模型含空白按鍵提取、复核不再默认显式 bypass。
- I-7:显式 + 非 sub2api 网关在 active 下已知不满足时从
“直接派发”变为“拦”(同款审计 action、error 文案、
blocked 标志);unknown-only 与 shadow 仍只审计。
- I-8:缺 key / 仓库门审计 detail 带任务 id 前缀,补进门拒绝
查询集合;仓库解析失败子任务归 gate_rejected、父走升级。
- I-10:gate_results 只列本轮实际跑过的门;显式与政策任务
两轮往返均不误触发 gate_missing。
- I-11:失败门 + 空 findings 时 evaluate 返回 escalate
(gate_failed_without_finding),不再抛 ValueError。
- I-13:分类优先读结构化审计行,子串只作最后回落且回落
命中时记 worker_coord_gate_classified_by_marker 审计行。
"""
import json
import logging
import os
import sqlite3
import sys
import tempfile
import time
import unittest
from pathlib import Path
from types import SimpleNamespace
try:
from cloud.audit_loop import Finding, evaluate
from cloud.auth import AuthStore
from cloud.models import (
RoutingPolicy, evaluate_capability, parse_model_capabilities,
parse_routing_policy)
from cloud.routing_shadow import (
evaluate_shadow, parse_worker_routing_detail)
from cloud.server import init_db
from cloud.worker import Worker, _GATE_ERROR_MARKERS
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.audit_loop import Finding, evaluate
from cloud.auth import AuthStore
from cloud.models import (
RoutingPolicy, evaluate_capability, parse_model_capabilities,
parse_routing_policy)
from cloud.routing_shadow import (
evaluate_shadow, parse_worker_routing_detail)
from cloud.server import init_db
from cloud.worker import Worker, _GATE_ERROR_MARKERS
def _insert_task(db_path, task_id, model=None, status="queued",
project_id="p1", audit_loop=0, parent=None, part=None,
error=None, result=None):
now = time.time()
conn = sqlite3.connect(db_path)
try:
conn.execute(
"INSERT INTO tasks(id, project_id, prompt, status,"
" idempotency_key, depends_on, priority, created_by,"
" created_at, updated_at, last_heartbeat, audit_loop,"
" parent_task_id, coordinator_part, result_text, error_text,"
" model)"
" VALUES(?, ?, 'do work', ?, NULL, '[]', 0, 'tester',"
" ?, ?, NULL, ?, ?, ?, ?, ?, ?)",
(task_id, project_id, status, now, now, audit_loop,
parent, part, result, error, model),
)
conn.commit()
finally:
conn.close()
def _set_started_at(db_path, task_id, started_at):
conn = sqlite3.connect(db_path)
try:
conn.execute(
"UPDATE tasks SET started_at = ? WHERE id = ?",
(started_at, task_id))
conn.commit()
finally:
conn.close()
def _insert_audit(db_path, project_id, action, detail, created_at=None):
conn = sqlite3.connect(db_path)
try:
conn.execute(
"INSERT INTO audit_log(user_id, project_id, action, detail,"
" created_at) VALUES(NULL, ?, ?, ?, ?)",
(project_id, action, detail,
created_at if created_at is not None else time.time()))
conn.commit()
finally:
conn.close()
def _audit_actions(db_path, project_id="p1"):
store = AuthStore(db_path)
try:
return [(e["action"], e["detail"]) for e in store.list_audit(
project_id)]
finally:
store.conn.close()
def _completed_runner(cmd):
name = cmd[cmd.index("--name") + 1] if "dispatch" in cmd else "t"
return SimpleNamespace(
returncode=0,
stdout=json.dumps({"task": name, "status": "completed",
"text": "done"}),
stderr="")
def _failing_runner(cmd):
name = cmd[cmd.index("--name") + 1] if "dispatch" in cmd else "t"
return SimpleNamespace(
returncode=0,
stdout=json.dumps({"task": name, "status": "failed",
"text": "boom"}),
stderr="")
def _worker(db_path, policy_doc=None, env=None, gateway=None,
runner=None):
calls = []
def recording(cmd):
calls.append(list(cmd))
return (runner or _completed_runner)(cmd)
worker = Worker(db_path, "/fake/host.py", runner=recording,
env=env or {"XCM_API_KEY": "test-placeholder"},
routing_policy=policy_doc)
if gateway is not None:
worker._ensure_gateway = lambda task: gateway
worker._sub2api_default_model = lambda e: "gateway-model"
worker._calls = calls
return worker
def _state_row(db_path, task_id):
conn = sqlite3.connect(db_path)
try:
return conn.execute(
"SELECT round, passed_gates, last_decision"
" FROM task_audit_loop_state WHERE task_id = ?",
(task_id,)).fetchone()
finally:
conn.close()
def _task_row(db_path, task_id):
conn = sqlite3.connect(db_path)
try:
return conn.execute(
"SELECT status, error_text FROM tasks WHERE id = ?",
(task_id,)).fetchone()
finally:
conn.close()
class WhitespacePolicyTestCase(unittest.TestCase):
"""I-6:策略写入路径白名单。"""
def test_whitespace_tier_model_dropped_and_named(self):
with self.assertLogs("cloud.models", level="WARNING") as logs:
policy = parse_routing_policy({
"mode": "active",
"tiers": {"plan": "my model", "default": "ok-model"},
})
self.assertIsNotNone(policy)
self.assertEqual(policy.tiers, {"default": "ok-model"})
self.assertEqual(policy.dropped_tiers, ["plan"])
self.assertTrue(any("plan" in line for line in logs.output))
def test_whitespace_capability_key_dropped_is_unknown(self):
caps = parse_model_capabilities({
"my model": {"tool_calling": True},
"ok": {"tool_calling": True},
})
self.assertEqual(caps, {"ok": {"tool_calling": True}})
verdict = evaluate_capability(caps, "my model",
{"tool_calling": True})
self.assertFalse(verdict.ok)
self.assertTrue(verdict.unknown)
self.assertEqual(verdict.unknown_missing, ["tool_calling"])
self.assertEqual(verdict.known_missing, [])
def test_whitespace_fallback_and_direct_tiers_dropped(self):
policy = RoutingPolicy(
tiers={"default": "my model", "plan": "ok"},
fallback_models=["my model", "ok"])
self.assertEqual(policy.tiers, {"plan": "ok"})
self.assertEqual(policy.fallback_models, ["ok"])
def test_shadow_row_with_whitespace_chosen_parses(self):
parsed = parse_worker_routing_detail(
"T9 tier=default source=policy applied=true"
" suggested=- chosen=my model")
self.assertIsNotNone(parsed)
self.assertEqual(parsed["chosen_model"], "my model")
# 进样本计数,不再整行丢弃。
report = evaluate_shadow([
{"detail": "T9 tier=default source=policy applied=true"
" suggested=- chosen=my model",
"created_at": 1000.0}], {}, {})
self.assertEqual(report["routed_task_n"], 1)
self.assertEqual(report["dropped_rows"]["unparseable"], 0)
def test_legacy_suggested_whitespace_still_counted_not_lost(self):
# suggested 是中间字段,仍要求无空白:legacy 含空白
# suggested 的行解析失败,但按批 2 计入丢行桶,不静默消失。
parsed = parse_worker_routing_detail(
"T9 tier=default source=policy applied=true"
" suggested=my model chosen=ok")
self.assertIsNone(parsed)
report = evaluate_shadow([
{"detail": "T9 tier=default source=policy applied=true"
" suggested=my model chosen=ok",
"created_at": 1000.0}], {}, {})
self.assertEqual(report["routed_task_n"], 0)
self.assertEqual(report["dropped_rows"]["unparseable"], 1)
class PersistedWhitespaceModelTestCase(unittest.TestCase):
"""I-6:含空白写回模型的复核不再被 bypass。"""
def setUp(self):
self._old = os.environ.get("CODEX_PUSH_ALLOW_PRIVATE_TARGETS")
os.environ["CODEX_PUSH_ALLOW_PRIVATE_TARGETS"] = "1"
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
store = AuthStore(self.db_path)
store.conn.close()
def tearDown(self):
if self._old is None:
os.environ.pop("CODEX_PUSH_ALLOW_PRIVATE_TARGETS", None)
else:
os.environ[CODEX_PUSH_ALLOW_PRIVATE_TARGETS] = self._old
self.tmp.cleanup()
def _policy(self):
return {
"mode": "active",
"tiers": {"default": {"model": "policy-model",
"requires": {"tool_calling": True}}},
"model_capabilities": {
"my model": {"tool_calling": False},
},
}
def test_persisted_whitespace_model_matches_and_is_rechecked(self):
_insert_audit(self.db_path, "p1", "worker_model_persisted",
"T1 tier=default model=my model")
worker = _worker(self.db_path, self._policy())
audits = []
worker._audit = lambda p, a, d="": audits.append((p, a, d))
# 旧 split()[2] 定位置取在这里永远匹配不上(model
# 含空白被切成两段);新按键提取必须认出写回模型。
self.assertTrue(worker._is_persisted_model(
"T1", "p1", "my model"))
# 复核必须真正跑起来、不再默认显式 bypass:含空白的
# 模型名进不了能力目录(I-6 白名单已丢弃该键),故
# 复核结果是 unknown——按 fix5 分档只审计不拦;断言
# 点是复核确实跑了(有 recheck_unknown 审计行),
# 而不是悄悄按显式放行(那时连审计行都没有)。
# active 下 known 非空才拦,空白模型永远到不了
# known(目录里没有它的条目),这是白名单的直接
# 推论,不是复核 bypass。
error = worker._recheck_persisted_model({
"id": "T1", "project_id": "p1", "model": "my model"})
self.assertIsNone(error)
self.assertTrue([a for _p, a, _d in audits
if a == "worker_capability_recheck_unknown"])
self.assertTrue(any("my model" in d for _p, a, d in audits
if a == "worker_capability_recheck_unknown"))
def test_persisted_whitespace_model_mismatch_is_not_persisted(self):
_insert_audit(self.db_path, "p1", "worker_model_persisted",
"T1 tier=default model=my model")
worker = _worker(self.db_path, self._policy())
self.assertFalse(worker._is_persisted_model(
"T1", "p1", "other model"))
self.assertFalse(worker._is_persisted_model(
"T2", "p1", "my model"))
class ExplicitNonSub2apiGateTestCase(unittest.TestCase):
"""I-7:显式 + 非 sub2api 网关的能力门。"""
def setUp(self):
self._old = os.environ.get("CODEX_PUSH_ALLOW_PRIVATE_TARGETS")
os.environ["CODEX_PUSH_ALLOW_PRIVATE_TARGETS"] = "1"
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
def tearDown(self):
if self._old is None:
os.environ.pop("CODEX_PUSH_ALLOW_PRIVATE_TARGETS", None)
else:
os.environ[CODEX_PUSH_ALLOW_PRIVATE_TARGETS] = self._old
self.tmp.cleanup()
def _policy(self, mode, capabilities):
return {
"mode": mode,
"tiers": {"default": {"model": "policy-model",
"requires": {"tool_calling": True}}},
"model_capabilities": capabilities,
}
def test_known_missing_explicit_non_sub2api_is_blocked(self):
# 行为变更:旧行为是直接派发(能力门只在 sub2api
# 时跑),新行为是 active 下已知不满足即拦。
_insert_task(self.db_path, "E1", model="explicit-model")
worker = _worker(self.db_path, self._policy("active", {
"explicit-model": {"tool_calling": False},
}), gateway=("other-gw", {}))
result = worker.run_one()
self.assertEqual(result["status"], "failed")
self.assertFalse(worker._calls)
self.assertIn("lacks capability", result["error"])
actions = _audit_actions(self.db_path)
explicit = [d for a, d in actions
if a == "worker_capability_gateway_explicit"]
self.assertTrue(explicit)
self.assertIn("blocked=True", explicit[0])
self.assertTrue([a for a, _ in actions
if a == "worker_capability_gate_rejected"])
def test_unknown_explicit_non_sub2api_is_audited_not_blocked(self):
_insert_task(self.db_path, "E1", model="explicit-model")
worker = _worker(self.db_path, self._policy("active", {}),
gateway=("other-gw", {}))
result = worker.run_one()
self.assertEqual(result["status"], "completed")
self.assertTrue(worker._calls)
actions = _audit_actions(self.db_path)
explicit = [d for a, d in actions
if a == "worker_capability_gateway_explicit"]
self.assertTrue(explicit)
self.assertIn("blocked=False", explicit[0])
self.assertFalse([a for a, _ in actions
if a == "worker_capability_gate_rejected"])
def test_shadow_explicit_non_sub2api_is_audited_not_blocked(self):
_insert_task(self.db_path, "E1", model="explicit-model")
worker = _worker(self.db_path, self._policy("shadow", {
"explicit-model": {"tool_calling": False},
}), gateway=("other-gw", {}))
result = worker.run_one()
self.assertEqual(result["status"], "completed")
self.assertTrue(worker._calls)
actions = _audit_actions(self.db_path)
explicit = [d for a, d in actions
if a == "worker_capability_gateway_explicit"]
self.assertTrue(explicit)
self.assertIn("blocked=False", explicit[0])
def test_nonexplicit_non_sub2api_has_no_gateway_gate(self):
# 非显式 + 非 sub2api 根本不进网关能力块:模型为空,
# 后续走 decide_routing,不存在独立门误拦。
_insert_task(self.db_path, "E1")
worker = _worker(self.db_path, self._policy("active", {
"policy-model": {"tool_calling": True},
}), gateway=("other-gw", {}))
result = worker.run_one()
self.assertEqual(result["status"], "completed")
actions = _audit_actions(self.db_path)
self.assertFalse([a for a, _ in actions
if a == "worker_capability_gateway_explicit"])
def test_explicit_no_gateway_keeps_explicit_semantics(self):
# 边界:显式 + 无网关(provider 为 None)不进网关
# 能力块——它根本没走网关,显式语义不变:已知不满足
# 也不在这里拦(写回模型仍走后续复核,用户显式仍
# 不被复核,fixes5 契约)。无网关显式任务被网关块
# 拦住会是语义倒退,不是 I-7 的意图。
_insert_task(self.db_path, "E1", model="explicit-model")
worker = _worker(self.db_path, self._policy("active", {
"explicit-model": {"tool_calling": False},
}), gateway=(None, {}))
result = worker.run_one()
self.assertEqual(result["status"], "completed")
self.assertTrue(worker._calls)
actions = _audit_actions(self.db_path)
self.assertFalse([a for a, _ in actions
if a == "worker_capability_gateway_explicit"])
self.assertFalse([a for a, _ in actions
if a == "worker_capability_gate_rejected"])
def test_explicit_sub2api_path_unchanged(self):
# sub2api 显式路径与原来逐字一致:同一审计 action、
# 同一 error 文案、同一 blocked 标志。
_insert_task(self.db_path, "E1", model="explicit-model")
worker = _worker(self.db_path, self._policy("active", {
"explicit-model": {"tool_calling": False},
}), gateway=("sub2api", {}))
result = worker.run_one()
self.assertEqual(result["status"], "failed")
self.assertFalse(worker._calls)
self.assertIn("lacks capability", result["error"])
actions = _audit_actions(self.db_path)
explicit = [d for a, d in actions
if a == "worker_capability_gateway_explicit"]
self.assertTrue(explicit)
self.assertIn("blocked=True", explicit[0])
class CoordGateAttributionTestCase(unittest.TestCase):
"""I-8 + I-13:协调员门拒绝归因与分类回落可见性。"""
def setUp(self):
self._old = os.environ.get("CODEX_PUSH_ALLOW_PRIVATE_TARGETS")
os.environ["CODEX_PUSH_ALLOW_PRIVATE_TARGETS"] = "1"
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
store = AuthStore(self.db_path)
store.conn.close()
self.audits = []
self.worker = Worker(self.db_path, "/fake/host.py",
runner=_completed_runner,
env={"XCM_API_KEY": "test-placeholder"},
max_retries=0, retry_backoff_seconds=0)
self.worker._audit = lambda p, a, d="": self.audits.append(
(p, a, d))
conn = sqlite3.connect(self.db_path)
try:
self.worker._ensure_result_columns(conn)
conn.commit()
finally:
conn.close()
def tearDown(self):
if self._old is None:
os.environ.pop("CODEX_PUSH_ALLOW_PRIVATE_TARGETS", None)
else:
os.environ[CODEX_PUSH_ALLOW_PRIVATE_TARGETS] = self._old
self.tmp.cleanup()
def _merge(self, child_error, audit_rows=()):
now = time.time()
_insert_task(self.db_path, "P1", status="running", audit_loop=1,
parent=None)
_insert_task(self.db_path, "C1", status="failed", parent="P1",
part="a", error=child_error)
_set_started_at(self.db_path, "C1", now - 100)
for action, detail in audit_rows:
_insert_audit(self.db_path, "p1", action, detail,
created_at=now)
out = self.worker._coord_audit_loop_merge(
{"id": "P1", "project_id": "p1", "audit_loop": 1},
[{"name": "a", "prompt": "do a"}],
[{"id": "C1", "name": "a"}])
return out
def test_repo_failure_child_is_gate_rejected_not_execution(self):
out = self._merge(
"仓库解析失败: bad repo",
audit_rows=[("worker_repo_failed",
"C1: 仓库解析失败: bad repo")])
self.assertEqual(out["decision"], "escalate")
self.assertIn("a", out.get("gate_rejected", []))
self.assertEqual(_task_row(self.db_path, "C1")[0], "failed")
# 结构化证据已命中,不走子串回落、无回落审计行。
self.assertFalse([a for _p, a, _d in self.audits
if a == "worker_coord_gate_classified_by_marker"])
def test_missing_key_child_is_gate_rejected_not_execution(self):
out = self._merge(
"Missing environment variable: OPENKUN_API_KEY",
audit_rows=[("worker_missing_key",
"C1: Missing environment variable:"
" OPENKUN_API_KEY")])
self.assertEqual(out["decision"], "escalate")
self.assertIn("a", out.get("gate_rejected", []))
self.assertEqual(_task_row(self.db_path, "C1")[0], "failed")
def test_recheck_rejected_child_is_gate_rejected(self):
out = self._merge(
"default tier persisted model m lacks capability",
audit_rows=[("worker_capability_recheck_rejected",
"C1: default tier persisted model m lacks"
" capability")])
self.assertEqual(out["decision"], "escalate")
self.assertIn("a", out.get("gate_rejected", []))
def test_structured_evidence_wins_over_reworded_error(self):
# error_text 措辞改掉(不含任何 marker 子串),结构化
# 审计行不变时分类仍为 gate_rejected:分类不再依赖文案。
out = self._merge(
"repo exploded unexpectedly",
audit_rows=[("worker_repo_failed",
"C1: 仓库解析失败: bad repo")])
self.assertEqual(out["decision"], "escalate")
self.assertIn("a", out.get("gate_rejected", []))
self.assertFalse([a for _p, a, _d in self.audits
if a == "worker_coord_gate_classified_by_marker"])
def test_marker_fallback_is_audited(self):
# 删结构化审计行、仅留 error_text 时分类走回落,
# 且合流里有回落审计行点名 marker。
out = self._merge("必需工具缺失: playwright")
self.assertEqual(out["decision"], "escalate")
self.assertIn("a", out.get("gate_rejected", []))
marker_rows = [d for _p, a, d in self.audits
if a == "worker_coord_gate_classified_by_marker"]
self.assertTrue(marker_rows)
self.assertIn("C1", marker_rows[0])
self.assertIn("marker=必需工具缺失", marker_rows[0])
def test_gate_lookup_failure_still_escalates(self):
# I-5 回归:审计表缺失导致查询失败时合流走 escalate,
# 不改回空集推回(与批 2 测试同款口径,本夹具先删表)。
_insert_task(self.db_path, "P1", status="running", audit_loop=1)
_insert_task(self.db_path, "C1", status="failed", parent="P1",
part="a", error="boom")
conn = sqlite3.connect(self.db_path)
try:
conn.execute("DROP TABLE IF EXISTS audit_log")
conn.commit()
finally:
conn.close()
out = self.worker._coord_audit_loop_merge(
{"id": "P1", "project_id": "p1", "audit_loop": 1},
[{"name": "a", "prompt": "do a"}],
[{"id": "C1", "name": "a"}])
self.assertEqual(out["decision"], "escalate")
self.assertTrue(out.get("gate_lookup_failed"))
self.assertEqual(_task_row(self.db_path, "C1")[0], "failed")
class GateResultsCheckedTestCase(unittest.TestCase):
"""I-10:gate_results 只列实际跑过的门 + 两轮不误升级。"""
def setUp(self):
self._old = os.environ.get("CODEX_PUSH_ALLOW_PRIVATE_TARGETS")
os.environ["CODEX_PUSH_ALLOW_PRIVATE_TARGETS"] = "1"
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
def tearDown(self):
if self._old is None:
os.environ.pop("CODEX_PUSH_ALLOW_PRIVATE_TARGETS", None)
else:
os.environ[CODEX_PUSH_ALLOW_PRIVATE_TARGETS] = self._old
self.tmp.cleanup()
def test_direct_call_checked_subset_and_legacy(self):
worker = _worker(self.db_path)
task = {"id": "T1", "project_id": "p1"}
gates, _ = worker._audit_loop_gate_results(
task, "failed", {"error": "boom"}, gates_checked=["gateway"])
self.assertEqual(gates, {"execution": False, "gateway": True})
gates, _ = worker._audit_loop_gate_results(
task, "failed", {"error": "boom"}, gates_checked=[])
self.assertEqual(gates, {"execution": False})
# 未传 gates_checked 的旧直接调用保持旧行为。
gates, _ = worker._audit_loop_gate_results(
task, "failed", {"error": "boom"})
self.assertEqual(gates["capability"], True)
self.assertEqual(gates["required_tools"], True)
self.assertEqual(gates["gateway"], True)
def test_policy_task_two_rounds_pushback_without_gate_missing(self):
_insert_task(self.db_path, "T2", audit_loop=1)
worker = _worker(self.db_path, {
"mode": "active",
"tiers": {"default": {"model": "policy-model",
"requires": {"tool_calling": True}}},
"model_capabilities": {
"policy-model": {"tool_calling": True},
},
}, gateway=(None, {}), runner=_failing_runner)
for _ in range(2):
out = worker.run_one()
self.assertEqual(out["status"], "queued")
row = _state_row(self.db_path, "T2")
self.assertIsNotNone(row)
self.assertEqual(row[0], 2)
# 政策任务本轮实际跑过的门只有 capability(网关桩
# 回 None、无 agent_tools 故工具门未跑),状态里
# passed_gates 不含没跑过的门。
self.assertEqual(json.loads(row[1]), ["capability"])
self.assertEqual(row[2], "pushback")
def test_explicit_task_two_rounds_pushback_without_gate_missing(self):
_insert_task(self.db_path, "T3", model="explicit-model",
audit_loop=1)
worker = _worker(self.db_path, gateway=(None, {}),
runner=_failing_runner)
for _ in range(2):
out = worker.run_one()
self.assertEqual(out["status"], "queued")
row = _state_row(self.db_path, "T3")
self.assertIsNotNone(row)
self.assertEqual(row[0], 2)
self.assertEqual(row[2], "pushback")
class EvaluateNoFindingTestCase(unittest.TestCase):
"""I-11:失败门 + 空 findings 返回 escalate,不抛异常。"""
def test_failed_gate_without_finding_escalates(self):
out = evaluate(None, "T1", {"execution": False}, findings=[])
self.assertEqual(out["decision"], "escalate")
self.assertEqual(out["reason"], "gate_failed_without_finding")
self.assertEqual(out["round"], 0)
self.assertIsNone(out["packet"])
self.assertEqual(out["new_state"].round, 0)
self.assertEqual(out["new_state"].last_decision, "escalate")
def test_failed_gate_without_finding_keeps_round(self):
from cloud.audit_loop import LoopState
state = LoopState("T1", round=2, passed_gates=["execution"],
criteria={}, last_decision="pushback")
# 注意:上一轮通过的门本轮变失败会生成 regression
# finding(all_findings 非空),走不到本支;所以
# 本例让失败的是上一轮未通过过的门(capability),
# 且上一轮通过的 execution 本轮仍通过——无回归、
# 无缺门、无弱化、无 finding,只有失败门。
out = evaluate(state, "T1",
{"execution": True, "capability": False},
findings=[])
self.assertEqual(out["decision"], "escalate")
self.assertEqual(out["reason"], "gate_failed_without_finding")
self.assertEqual(out["new_state"].round, 2)
def test_finding_still_pushbacks_and_max_rounds_unchanged(self):
finding = Finding("execution_failed", "high", "boom", "re-run",
"task completes", kind="deterministic",
gate="execution")
out = evaluate(None, "T1", {"execution": False},
findings=[finding])
self.assertEqual(out["decision"], "pushback")
self.assertEqual(out["round"], 1)
from cloud.audit_loop import LoopState
state = LoopState("T1", round=3, passed_gates=[],
criteria={}, last_decision="pushback")
out = evaluate(state, "T1", {"execution": False},
findings=[finding])
self.assertEqual(out["decision"], "escalate")
self.assertEqual(out["reason"], "max_rounds_exceeded")
class GateErrorMarkersTestCase(unittest.TestCase):
"""I-13:分类子串表抽成模块常量。"""
def test_marker_constant_covers_known_markers(self):
self.assertIn("Missing environment variable:",
_GATE_ERROR_MARKERS)
self.assertIn("网关准备失败", _GATE_ERROR_MARKERS)
self.assertIn("必需工具缺失", _GATE_ERROR_MARKERS)
self.assertIn("工具审批声明矛盾", _GATE_ERROR_MARKERS)
self.assertIn("refusing to dispatch an incapable or"
" unverifiable model", _GATE_ERROR_MARKERS)
self.assertIn("fails capability", _GATE_ERROR_MARKERS)
if __name__ == "__main__":
unittest.main()