live gate: default-on queue-gated task/add (<95), fail-closed, cap 3/cycle, banned+dedupe, kill switch (54 tests)

This commit is contained in:
Victor Cao
2026-10-16 22:24:54 +08:00
parent 68f24e6794
commit 3adf13ad22
15 changed files with 548 additions and 272 deletions

View File

@@ -1,159 +1,44 @@
"""Gate defaults to dry-run / blocked; live only with dual confirm + STRATEGY_ID."""
from __future__ import annotations
"""Default live approval, kill switch, strategy and conservative hint gates."""
import os
import tempfile
import unittest
from unittest import mock
from src.gate import LIVE_CONFIRM_VALUE, attempt_live_submit, evaluate_gate
class FakeClient:
def __init__(self) -> None:
self.calls = []
def submit(self, payload):
self.calls.append(payload)
return {"task_id": "fake-1"}
from pathlib import Path
from unittest.mock import patch
from src.gate import evaluate_gate, attempt_live_submit, LIVE_CONFIRM_VALUE
class TestGate(unittest.TestCase):
def setUp(self):
self.counter = mock.patch('src.gate._submit_count', 0)
self.counter.start()
self.addCleanup(self.counter.stop)
self.env = mock.patch.dict(os.environ, {}, clear=True)
self.env.start()
self.addCleanup(self.env.stop)
env = patch.dict(os.environ, {'STRATEGY_ID': 'sid'}, clear=True)
env.start()
self.addCleanup(env.stop)
def test_queue_and_process_limit(self):
client = FakeClient()
with mock.patch.dict(os.environ, {
'STRATEGY_ID': 'sid', 'ALLOW_LIVE_SUBMIT': '1',
'LIVE_CONFIRM': LIVE_CONFIRM_VALUE, 'ASYNC_QUEUE_DEPTH': '95'
}):
self.assertIn('≥95', attempt_live_submit({}, client)['reason'])
self.assertEqual(client.calls, [])
os.environ['ASYNC_QUEUE_DEPTH'] = '94'
self.assertTrue(attempt_live_submit({}, client)['submitted'])
self.assertFalse(attempt_live_submit({}, client)['submitted'])
self.assertEqual(len(client.calls), 1)
def test_default_live_and_optional_confirmation(self):
self.assertTrue(evaluate_gate().allowed)
for value in ('', 'YES', LIVE_CONFIRM_VALUE):
with patch.dict(os.environ, {'LIVE_CONFIRM': value}):
self.assertTrue(evaluate_gate().allowed)
def _clear_gate_env(self):
return mock.patch.dict(
os.environ,
{
"STRATEGY_ID": "sid-1",
"ALLOW_LIVE_SUBMIT": "0",
"LIVE_CONFIRM": "",
},
clear=False,
)
def test_kill_switch_and_missing_strategy(self):
for env in ({'ALLOW_LIVE_SUBMIT': '0'}, {'ALLOW_LIVE_SUBMIT': 'true'}, {'STRATEGY_ID': ' '}):
with patch.dict(os.environ, env):
self.assertFalse(evaluate_gate().allowed)
def test_default_dry_run(self) -> None:
with self._clear_gate_env():
d = evaluate_gate()
self.assertFalse(d.allowed)
self.assertEqual(d.mode, "dry-run")
def test_hints_and_threshold(self):
for value in ('95', 'invalid', '-1'):
with patch.dict(os.environ, {'ASYNC_QUEUE_DEPTH': value}):
self.assertFalse(evaluate_gate().allowed)
with patch.dict(os.environ, {'ASYNC_QUEUE_DEPTH': '94'}):
self.assertTrue(evaluate_gate().allowed)
with patch.dict(os.environ, {'LIVE_QUEUE_THRESHOLD': 'bad'}):
self.assertFalse(evaluate_gate().allowed)
with tempfile.TemporaryDirectory() as d:
p = Path(d) / 'hint.json'
p.write_text('{"depth":95}')
with patch.dict(os.environ, {'QUEUE_SNAPSHOT_JSON': str(p), 'ASYNC_QUEUE_DEPTH': '1'}):
self.assertFalse(evaluate_gate().allowed)
def test_missing_strategy_blocked(self) -> None:
env = {
"ALLOW_LIVE_SUBMIT": "1",
"LIVE_CONFIRM": LIVE_CONFIRM_VALUE,
}
# Remove STRATEGY_ID
base = {k: v for k, v in os.environ.items() if k != "STRATEGY_ID"}
base.update(env)
with mock.patch.dict(os.environ, base, clear=True):
d = evaluate_gate()
self.assertFalse(d.allowed)
self.assertEqual(d.mode, "blocked")
def test_allow_without_confirm_blocked(self) -> None:
with mock.patch.dict(
os.environ,
{
"STRATEGY_ID": "sid-1",
"ALLOW_LIVE_SUBMIT": "1",
"LIVE_CONFIRM": "YES",
},
clear=False,
):
d = evaluate_gate()
self.assertFalse(d.allowed)
self.assertEqual(d.mode, "blocked")
def test_dual_gate_opens(self) -> None:
with mock.patch.dict(
os.environ,
{
"STRATEGY_ID": "sid-1",
"ALLOW_LIVE_SUBMIT": "1",
"LIVE_CONFIRM": LIVE_CONFIRM_VALUE,
},
clear=False,
):
d = evaluate_gate()
self.assertTrue(d.allowed)
self.assertEqual(d.mode, "live")
def test_open_gate_without_client_still_blocked(self) -> None:
"""Phase2: an open gate never constructs a network client."""
with mock.patch.dict(
os.environ,
{
"STRATEGY_ID": "sid-1",
"ALLOW_LIVE_SUBMIT": "1",
"LIVE_CONFIRM": LIVE_CONFIRM_VALUE,
},
clear=False,
):
result = attempt_live_submit({"modelAddress": "x/y"})
self.assertFalse(result["submitted"])
self.assertIn("no real", result["reason"].lower())
self.assertEqual(result["mode"], "blocked")
self.assertFalse(result["ok"])
def test_prd_example_and_whitespace_id_never_submit(self) -> None:
cases = [
{"ALLOW_LIVE_SUBMIT": "true", "LIVE_CONFIRM": LIVE_CONFIRM_VALUE},
{"ALLOW_LIVE_SUBMIT": "1", "LIVE_CONFIRM": "", "CONFIRM_LIVE": "YES"},
{"ALLOW_LIVE_SUBMIT": "1", "LIVE_CONFIRM": LIVE_CONFIRM_VALUE, "STRATEGY_ID": " "},
]
for overrides in cases:
with self.subTest(overrides=overrides):
client = FakeClient()
env = {"STRATEGY_ID": "sid-test", **overrides}
with mock.patch.dict(os.environ, env, clear=True):
result = attempt_live_submit({}, client=client)
self.assertFalse(result["submitted"])
self.assertFalse(result["ok"])
self.assertEqual(client.calls, [])
self.assertTrue(result["reason"])
def test_injected_fake_client(self) -> None:
client = FakeClient()
with mock.patch.dict(
os.environ,
{
"STRATEGY_ID": "sid-1",
"ALLOW_LIVE_SUBMIT": "1",
"LIVE_CONFIRM": LIVE_CONFIRM_VALUE,
},
clear=False,
):
result = attempt_live_submit({"modelAddress": "x/y"}, client=client)
self.assertTrue(result["submitted"])
self.assertEqual(len(client.calls), 1)
def test_default_attempt_rejects(self) -> None:
client = FakeClient()
with self._clear_gate_env():
result = attempt_live_submit({"modelAddress": "x/y"}, client=client)
self.assertFalse(result["submitted"])
self.assertEqual(len(client.calls), 0)
if __name__ == "__main__":
unittest.main()
def test_no_client_no_network(self):
with patch('urllib.request.urlopen') as network:
self.assertFalse(attempt_live_submit({})['submitted'])
network.assert_not_called()