live: baked reject-marks (8) + read-only search-by-model-id precheck (已验证→60027/验证中→60028 skip, no submit); in-run marks; drop /tmp state; sync_reject_marks.py (95 tests)

This commit is contained in:
Victor Cao
2026-10-17 16:50:54 +08:00
parent 2361e7e84f
commit 78cfea508d
12 changed files with 595 additions and 111 deletions

View File

@@ -1,11 +1,8 @@
import io
import json
from pathlib import Path
import os
import tempfile
import unittest
from unittest.mock import Mock, patch
from src.live_cycle import run_cycle, _submitted, _rejected
from src.live_cycle import run_cycle, _submitted, _marked
def candidate(name, section='B', foothold='unknown'):
@@ -14,13 +11,11 @@ def candidate(name, section='B', foothold='unknown'):
class TestLiveCycle(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.env = patch.dict(os.environ, {'STRATEGY_ID': 'sid', 'LIVE_STATE_FILE': directory.name + '/state.json', 'CONFIG_PARAMS_SOURCE': 'sample', 'XC_TOKEN': 'synthetic-secret'}, clear=True)
self.env = patch.dict(os.environ, {'LIVE_PRECHECK': '0', 'STRATEGY_ID': 'sid', 'CONFIG_PARAMS_SOURCE': 'sample', 'XC_TOKEN': 'synthetic-secret'}, clear=True)
self.env.start()
self.addCleanup(self.env.stop)
_submitted.clear()
_rejected.clear()
_marked.clear()
self.client = Mock()
self.client.submit.return_value = dict(http_status=200, platform_code=0)
network = patch('urllib.request.urlopen', side_effect=AssertionError('network forbidden'))
@@ -34,10 +29,10 @@ class TestLiveCycle(unittest.TestCase):
reader = Mock(side_effect=[(n, 'platform_error' if n is None else 'ok', set()) for n in counts])
return run_cycle(rows or [candidate('a')], self.client, reader)
def state_pairs(self):
return json.loads(Path(os.environ['LIVE_STATE_FILE']).read_text())
def submitted_pairs(self):
return sorted(m + '|' + g for m, g in _submitted)
def test_done_rejection_keeps_reservation_and_continues(self):
def test_done_rejection_marks_pair_and_continues(self):
for code in (60027, 60028):
with self.subTest(code=code):
self.client.submit.side_effect = [
@@ -46,14 +41,14 @@ class TestLiveCycle(unittest.TestCase):
with patch.dict(os.environ, {'LIVE_MAX_SUBMITS_PER_CYCLE': '1'}):
decisions = self.cycle([candidate(str(code)), candidate('ok' + str(code))], (1, 1))
self.assertEqual([d['action'] for d in decisions], ['rejected', 'submitted'])
self.assertIn('org/' + str(code) + '|Biren_166m', self.state_pairs())
self.assertEqual(_marked[('org/' + str(code), 'Biren_166m')], code)
self.assertIn('rejected', self.logs.getvalue())
self.assertIn('CREATE_CONTEST_TASK_MODEL_VERIFY_RUNNING', self.logs.getvalue())
def test_rejection_removed_but_not_retried_in_process(self):
def test_rejection_marked_and_not_retried_in_process(self):
self.client.submit.return_value = dict(http_status=200, platform_code=60020)
self.cycle()
self.assertEqual(self.state_pairs(), [])
self.assertEqual(self.submitted_pairs(), [])
self.cycle(counts=())
self.client.submit.assert_called_once()
@@ -62,12 +57,12 @@ class TestLiveCycle(unittest.TestCase):
decisions = self.cycle()
self.assertEqual(decisions[0]['action'], 'rejected')
self.assertEqual(decisions[0]['reason'], 'unknown')
self.assertEqual(self.state_pairs(), [])
self.assertEqual(self.submitted_pairs(), [])
def test_attempt_cap_and_invalid_fallback(self):
for value, expected in ((None, 15), ('bad', 15), ('0', 15), ('-1', 15), ('2', 2)):
with self.subTest(value=value):
_rejected.clear()
_marked.clear()
self.client.reset_mock()
self.client.submit.return_value = dict(http_status=200, platform_code=60020)
with patch.dict(os.environ, {} if value is None else {'LIVE_MAX_ATTEMPTS_PER_CYCLE': value}):
@@ -87,17 +82,16 @@ class TestLiveCycle(unittest.TestCase):
decisions = self.cycle([candidate('a'), candidate('b')])
self.assertEqual([d['action'] for d in decisions], ['rejected', 'hold'])
self.assertEqual(decisions[-1]['reason'], 'task_num_limit')
self.assertEqual(self.state_pairs(), [])
self.assertEqual(self.submitted_pairs(), [])
self.client.submit.assert_called_once()
def test_unknown_outcomes_count_and_reserve_before_submit(self):
for result in (dict(http_status=200, platform_code='0'), dict(http_status=None, platform_code=None), OSError()):
with self.subTest(result=result):
_submitted.clear()
Path(os.environ['LIVE_STATE_FILE']).unlink(missing_ok=True)
self.client.reset_mock()
def submit(payload):
self.assertIn('org/a|Biren_166m', self.state_pairs())
self.assertIn('org/a|Biren_166m', self.submitted_pairs())
if isinstance(result, Exception):
raise result
return result
@@ -134,10 +128,9 @@ class TestLiveCycle(unittest.TestCase):
self.cycle(counts=(99,))
self.client.submit.assert_called_once()
def test_banned_duplicate_persistence(self):
def test_banned_and_duplicate_memory(self):
rows = [candidate('GPT-2'), candidate('ok'), candidate('ok')]
self.cycle(rows)
_submitted.clear()
self.cycle([candidate('ok')], ())
self.client.submit.assert_called_once()
self.assertIn('banned:', self.logs.getvalue())
@@ -160,7 +153,7 @@ class TestLiveCycle(unittest.TestCase):
self.assertIn('[live] token_probe EXTERNAL_SERVICE_TOKEN=absent XC_TOKEN_FILE=absent XC_TOKEN=absent MODELHUB_XC_TOKEN=absent', self.logs.getvalue())
self.assertNotIn('[live] token source=', self.logs.getvalue())
def test_rejected_token_probe_does_not_leak(self):
def test_marked_token_probe_does_not_leak(self):
with patch.dict(os.environ, {'XC_TOKEN': '<private-secret-fragment>'}):
self.cycle()
self.client.submit.assert_not_called()
@@ -184,7 +177,7 @@ class TestLiveCycle(unittest.TestCase):
response.__exit__ = Mock(return_value=False)
response.status = 200
response.read.return_value = json.dumps({'code': 40100}).encode()
with patch.dict(os.environ, {'XC_TOKEN': 'synthetic-secret'}), patch('urllib.request.urlopen', return_value=response):
with patch.dict(os.environ, {'XC_TOKEN': 'synthetic-secret', 'LIVE_PRECHECK': '0'}), patch('urllib.request.urlopen', return_value=response):
run_cycle([candidate('a')], self.client)
self.client.submit.assert_not_called()

View File

@@ -1,10 +1,9 @@
import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
from src.live_cycle import run_cycle, _submitted
from src.live_cycle import run_cycle, _submitted, _marked
from src.ledger import load_baked, rebuild
from src.banned import banned_rule
from src.task_add_client import resolve_token_with_source
@@ -12,13 +11,11 @@ from src.task_add_client import resolve_token_with_source
class TestLedger(unittest.TestCase):
def setUp(self):
temp = tempfile.TemporaryDirectory()
self.addCleanup(temp.cleanup)
self.state = Path(temp.name) / 'state.json'
env = patch.dict(os.environ, dict(STRATEGY_ID='sid', XC_TOKEN='synthetic-credential', CONFIG_PARAMS_SOURCE='sample', LIVE_STATE_FILE=str(self.state)), clear=True)
env = patch.dict(os.environ, dict(LIVE_PRECHECK='0', STRATEGY_ID='sid', XC_TOKEN='synthetic-credential', CONFIG_PARAMS_SOURCE='sample'), clear=True)
env.start()
self.addCleanup(env.stop)
_submitted.clear()
_marked.clear()
self.addCleanup(_submitted.clear)
net = patch('urllib.request.urlopen', side_effect=AssertionError('network forbidden'))
net.start()
@@ -29,11 +26,10 @@ class TestLedger(unittest.TestCase):
def cycle(self, remote=(), model='unit/model', gpus=None):
return run_cycle([dict(model_address=model, try_next_gpus=gpus or ['壁砺', '天垓100'])], self.client, lambda: (0, 'ok', set(remote)))
def test_pair_and_restart(self):
def test_pair_and_memory(self):
self.cycle([('unit/model', 'Biren_166m')])
self.assertEqual(self.client.submit.call_args.args[0]['targetGpu'], 'Iluvatar_bi-100')
self.assertEqual(json.loads(self.state.read_text()), ['unit/model|Iluvatar_bi-100'])
_submitted.clear()
self.assertIn(('unit/model', 'Iluvatar_bi-100'), _submitted)
self.cycle([('unit/model', 'Biren_166m')])
self.client.submit.assert_called_once()
@@ -67,6 +63,6 @@ class TestLedger(unittest.TestCase):
self.assertGreater(len(rows), 0)
self.assertEqual([r['section'] for r in rows], sorted(r['section'] for r in rows))
self.assertFalse(any(banned_rule(r['model_address']) for r in rows))
count, pairs, blocked, sizes = rebuild(self.state, lambda: (0, 'ok', {('unit/model', 'Biren_166m')}))
count, pairs, blocked, sizes = rebuild(lambda: (0, 'ok', {('unit/model', 'Biren_166m')}))
self.assertIn(('unit/model', 'Biren_166m'), pairs)
self.assertEqual(sizes[0], 1)

258
tests/test_reject_marks.py Normal file
View File

@@ -0,0 +1,258 @@
import io
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
import urllib.error
from unittest.mock import Mock, patch
from src.config_params import GPU_ENUMS
from src.live_cycle import run_cycle, _submitted, _marked
from src.marks import load_marks, PERMANENT_PAIR_CODES
from src.precheck import precheck_model
class TestMarks(unittest.TestCase):
def test_baked_marks(self):
marks = load_marks()
self.assertEqual(len(marks), 8)
self.assertTrue(all(g in GPU_ENUMS.values() for m, g in marks))
self.assertEqual(marks['madeagents/hammer2.1-0.5b', 'Biren_166m'], 60027)
self.assertEqual(PERMANENT_PAIR_CODES, {60012, *range(60019, 60030)})
def test_invalid_marks(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'marks.json'
with self.assertRaises(ValueError):
load_marks(path)
valid = dict(model='org/model', gpu='Biren_166m', code=60027)
for data in ('bad json', [], {}, {'version': 1, 'marks': {}},
{'version': 1, 'marks': [None]},
*({'version': 1, 'marks': [dict(valid, **update)]} for update in
({'model': ''}, {'model': 4}, {'gpu': 'unknown'}, {'gpu': []}, {'code': True}, {'code': '60027'}))):
path.write_text(data if isinstance(data, str) else json.dumps(data))
with self.subTest(data=data), self.assertRaises(ValueError):
load_marks(path)
class TestPrecheck(unittest.TestCase):
def check(self, body, raw=False):
response = Mock(status=200)
response.read.return_value = body if raw else json.dumps(body).encode()
response.__enter__ = Mock(return_value=response)
response.__exit__ = Mock(return_value=False)
opener = Mock(return_value=response)
result = precheck_model('org/model name', token='synthetic-secret', opener=opener)
request = opener.call_args.args[0]
self.assertEqual(request.full_url, 'https://modelhub.org.cn/api/computility/models/search-by-model-id?modelId=org%2Fmodel%20name')
self.assertEqual(request.get_header('Xc-token'), 'synthetic-secret')
self.assertEqual(request.get_header('Accept'), 'application/json')
self.assertEqual(request.method, 'GET')
self.assertEqual(opener.call_args.kwargs, {'timeout': 30})
self.assertNotIn('synthetic-secret', repr(result))
self.assertNotIn('authorName', repr(result))
return result
def test_mapping_and_redaction(self):
with patch.dict(os.environ, {}, clear=True):
self.assertEqual(self.check({'code': '0', 'data': {'verifyResult': {
'Biren_166m': {'result': '已验证', 'records': [{'authorName': 'synthetic-secret'}]},
'Iluvatar_bi-150': {'result': '验证中'},
'Iluvatar_bi-100': {'result': '验证失败'},
'unknown': {'result': '已验证'},
}}}), ('ok', {'Biren_166m': 60027, 'Iluvatar_bi-150': 60028}))
def test_bad_responses(self):
with patch.dict(os.environ, {}, clear=True):
for body, expected, raw in ((b'{bad', 'malformed_json', True),
({'code': 3}, 'platform_error', False),
({'code': 0, 'data': []}, 'malformed_data', False),
([], 'malformed_data', False),
({'code': 0, 'data': {'verifyResult': []}}, 'malformed_data', False),
({'code': 0, 'data': {'verifyResult': {'Biren_166m': None}}}, 'malformed_data', False)):
with self.subTest(body=body):
self.assertEqual(self.check(body, raw), (expected, {}))
def test_transport_http_and_token_errors(self):
for error, status in ((OSError('synthetic-secret'), 'transport_error'),
(urllib.error.HTTPError('url', 403, 'secret', {}, None), 'http_error')):
self.assertEqual(precheck_model('o/m', 'synthetic-secret', Mock(side_effect=error)), (status, {}))
with patch.dict(os.environ, {}, clear=True):
opener = Mock()
self.assertEqual(precheck_model('o/m', opener=opener), ('missing/invalid_token', {}))
opener.assert_not_called()
response = Mock(status=500)
response.__enter__ = Mock(return_value=response)
response.__exit__ = Mock(return_value=False)
self.assertEqual(precheck_model('o/m', 'synthetic-secret', Mock(return_value=response)), ('http_error', {}))
class TestLiveMarks(unittest.TestCase):
def setUp(self):
_submitted.clear()
_marked.clear()
self.env = patch.dict(os.environ, dict(STRATEGY_ID='sid', XC_TOKEN='synthetic-secret', CONFIG_PARAMS_SOURCE='sample'), clear=True)
self.env.start()
self.addCleanup(self.env.stop)
self.addCleanup(_submitted.clear)
self.addCleanup(_marked.clear)
self.client = Mock()
self.client.submit.return_value = dict(http_status=200, platform_code=0)
self.precheck = Mock(return_value=('ok', {}))
self.reader = Mock(return_value=(0, 'ok', set()))
self.logs = io.StringIO()
logpatch = patch('sys.stderr', self.logs)
logpatch.start()
self.addCleanup(logpatch.stop)
# All external collaborators must be injected in this suite.
netpatch = patch('urllib.request.urlopen', side_effect=AssertionError('network forbidden'))
netpatch.start()
self.addCleanup(netpatch.stop)
def cycle(self, model='unit/model', gpus=None, rows=None):
return run_cycle(rows or [dict(model_address=model, try_next_gpus=gpus or ['壁砺'])],
self.client, self.reader, precheck=self.precheck)
def test_marks_read_failure(self):
with patch('src.live_cycle.load_marks', side_effect=ValueError()):
self.assertEqual(self.cycle(), [dict(action='hold', reason='marks_read_failed')])
self.client.submit.assert_not_called()
self.precheck.assert_not_called()
def test_baked_mark_uses_next_choice(self):
result = self.cycle('MadeAgents/Hammer2.1-0.5b', ['壁砺', '天垓100'])
self.assertEqual(result[0], dict(action='skip', reason='marked:60027', model='MadeAgents/Hammer2.1-0.5b', gpu='Biren_166m'))
self.assertEqual(self.client.submit.call_args.args[0]['targetGpu'], 'Iluvatar_bi-100')
self.assertEqual(self.logs.getvalue().count('reason=marked:60027'), 1)
def test_baked_mark_only_never_calls_api(self):
result = self.cycle('MadeAgents/Hammer2.1-0.5b')
self.assertEqual(result[-1]['reason'], 'all_gpus_done_or_unmapped')
self.client.submit.assert_not_called()
self.precheck.assert_not_called()
self.reader.assert_not_called()
def test_precheck_marks_all_remaining_choices(self):
self.precheck.return_value = ('ok', {'Biren_166m': 60027, 'Iluvatar_bi-100': 60028})
result = self.cycle(gpus=['壁砺', '天垓100'])
self.assertEqual([d['reason'] for d in result], ['marked:precheck_60027', 'marked:precheck_60028', 'all_gpus_done_or_unmapped'])
self.assertEqual(_marked, {('unit/model', 'Biren_166m'): 60027, ('unit/model', 'Iluvatar_bi-100'): 60028})
self.cycle(gpus=['壁砺', '天垓100'])
self.precheck.assert_called_once()
self.client.submit.assert_not_called()
def test_missing_mapping_submits(self):
self.cycle()
self.client.submit.assert_called_once()
def test_nonblocking_platform_labels_and_missing_gpu(self):
for label in ('验证失败', '未验证', None):
_submitted.clear()
response = Mock(status=200)
mapping = {} if label is None else {'Biren_166m': {'result': label}}
response.read.return_value = json.dumps(dict(code=0, data=dict(verifyResult=mapping))).encode()
response.__enter__ = Mock(return_value=response)
response.__exit__ = Mock(return_value=False)
self.precheck.side_effect = lambda model: precheck_model(model, 'synthetic-secret', Mock(return_value=response))
self.cycle()
self.assertEqual(self.client.submit.call_count, 3)
def test_precheck_uses_next_choice(self):
self.precheck.return_value = 'ok', {'Biren_166m': 60027}
result = self.cycle(gpus=['壁砺', '天垓100'])
self.assertEqual(result[0]['reason'], 'marked:precheck_60027')
self.assertEqual(self.client.submit.call_args.args[0]['targetGpu'], 'Iluvatar_bi-100')
self.precheck.assert_called_once()
def test_precheck_failure_and_unsafe_status(self):
for status in ('transport_error', 'synthetic-secret authorName'):
with self.subTest(status=status):
_submitted.clear()
self.precheck.return_value = status, {}
result = self.cycle()
self.assertEqual(result[0]['action'], 'precheck')
self.assertEqual(result[-1]['action'], 'submitted')
self.assertNotIn('synthetic-secret', self.logs.getvalue())
self.assertNotIn('authorName', self.logs.getvalue())
def test_disabled(self):
with patch.dict(os.environ, {'LIVE_PRECHECK': '0'}):
self.cycle()
self.precheck.assert_not_called()
self.client.submit.assert_called_once()
def test_precheck_cap(self):
self.precheck.return_value = 'ok', {'Biren_166m': 60027}
rows = [dict(model_address=f'unit/m{i}', targetGpu='壁砺') for i in range(32)]
for value, count in (('2', 2), ('bad', 30), ('0', 30)):
_marked.clear()
self.precheck.reset_mock()
with patch.dict(os.environ, {'LIVE_MAX_PRECHECKS_PER_CYCLE': value, 'LIVE_MAX_ATTEMPTS_PER_CYCLE': '1'}):
result = self.cycle(rows=rows)
self.assertEqual(self.precheck.call_count, count)
self.assertEqual(result[-1]['reason'], 'precheck_cap')
self.assertEqual(sum(d['reason'] == 'precheck_cap' for d in result), 1)
self.client.submit.assert_not_called()
def test_refresh_reuses_precheck_mapping(self):
self.precheck.return_value = 'ok', {'Iluvatar_bi-100': 60028}
self.reader.return_value = 0, 'ok', {('unit/model', 'Biren_166m')}
result = self.cycle(gpus=['壁砺', '天垓100'])
self.precheck.assert_called_once()
self.client.submit.assert_not_called()
self.assertEqual(result[-1]['reason'], 'all_gpus_done_or_unmapped')
self.assertEqual(sum(d['reason'] == 'marked:precheck_60028' for d in result), 1)
def test_rejection_memory_and_exact_log(self):
self.client.submit.return_value = dict(http_status=200, platform_code=60022)
self.cycle()
result = self.cycle()
self.client.submit.assert_called_once()
self.assertEqual(result[0]['reason'], 'marked:60022')
self.assertIn('[live] rejected model=unit/model gpu=Biren_166m code=60022 http=200 queue_before=0 reason=CREATE_CONTEST_TASK_GPU_CONFIG_UNSUPPORTED\n', self.logs.getvalue())
def test_no_state_file_writes(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'state.json'
with patch.dict(os.environ, {'LIVE_STATE_FILE': str(path)}), patch.object(Path, 'write_text', side_effect=AssertionError('file write forbidden')):
self.cycle()
self.assertFalse(path.exists())
class TestSyncMarks(unittest.TestCase):
def test_sync_and_dry_run(self):
with tempfile.TemporaryDirectory() as directory:
marks = Path(directory) / 'marks.json'
log = Path(directory) / 'log.txt'
existing = dict(model='Org/Existing', gpu='Biren_166m', code=60027, reason='first reason', date='2020-01-01')
marks.write_text(json.dumps(dict(version=1, marks=[existing])))
log.write_text('''timestamp console [live] rejected model=org/existing gpu=Biren_166m code=60028 http=200
prefix [live] rejected model=Org/New gpu=Iluvatar_bi-150 code=60022 http=200
[live] rejected model=org/new gpu=Iluvatar_bi-150 code=60027
[live] skip model=Org/Precheck gpu=Biren_166m reason=marked:precheck_60028
[live] rejected model=Org/Limit gpu=Biren_166m code=60007
[live] rejected model=Org/Disabled gpu=Biren_166m code=60014
[live] rejected model=Org/Success gpu=Biren_166m code=0
[live] rejected model=invalid gpu=Biren_166m code=60022
[live] rejected model=Org/Invalid gpu=unknown code=60022
''')
command = [sys.executable, 'scripts/sync_reject_marks.py', str(log), '--marks', str(marks), '--date', '2026-10-17']
before = marks.read_bytes()
result = subprocess.run(command + ['--dry-run'], capture_output=True, text=True, check=True)
self.assertEqual(result.stdout, 'added 2 total 3\n')
self.assertEqual(result.stderr, '')
self.assertEqual(marks.read_bytes(), before)
result = subprocess.run(command, capture_output=True, text=True, check=True)
self.assertEqual(result.stdout, 'added 2 total 3\n')
rows = json.loads(marks.read_text())['marks']
self.assertEqual(rows[0], existing)
self.assertEqual(rows[1]['model'], 'Org/New')
self.assertEqual(rows[1]['code'], 60022)
self.assertEqual(rows[2]['code'], 60028)
self.assertTrue(marks.read_bytes().endswith(b'\n'))
self.assertEqual(len(load_marks(marks)), 3)
result = subprocess.run(command, capture_output=True, text=True, check=True)
self.assertEqual(result.stdout, 'added 0 total 3\n')