forked from luopingyi/xc_agent_platform_demo
Phase2 dry-run: pending A, probe, build-config client, gated task/add
Still default dry-run; ALLOW_LIVE_SUBMIT=0. Live gates closed.
This commit is contained in:
73
tests/test_build_config_client.py
Normal file
73
tests/test_build_config_client.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import runpy
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
import urllib.error
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from src.build_config_client import build_config, BuildConfigError
|
||||
from scripts.build_config_dry import main
|
||||
|
||||
|
||||
class TestBuildConfigClient(unittest.TestCase):
|
||||
def response(self, body, status=200):
|
||||
response = MagicMock()
|
||||
response.__enter__.return_value = response
|
||||
response.status = status
|
||||
response.read.return_value = json.dumps(body).encode()
|
||||
return response
|
||||
|
||||
def test_success_request_and_auth(self):
|
||||
for authorization in ('0', '1'):
|
||||
with patch.dict(os.environ, {'XC_TOKEN': 'test-secret', 'BUILD_CONFIG_USE_AUTHORIZATION': authorization}, clear=True), patch('urllib.request.urlopen', return_value=self.response({'code': 0, 'data': 'framework: vllm\n'})) as call:
|
||||
self.assertEqual(build_config('Sunrise_pt-200-x1'), 'framework: vllm\n')
|
||||
request = call.call_args.args[0]
|
||||
self.assertEqual(request.get_method(), 'POST')
|
||||
self.assertIsNone(request.data)
|
||||
parsed = urlparse(request.full_url)
|
||||
self.assertEqual(parsed.path, '/api/adapt/task/build-config')
|
||||
self.assertNotIn('/task/add', request.full_url)
|
||||
self.assertEqual(parse_qs(parsed.query), {'gpuType': ['Sunrise_pt-200-x1'], 'framework': ['vllm'], 'taskType': ['text-generation']})
|
||||
self.assertEqual(request.get_header('Authorization' if authorization == '1' else 'Xc-token'), 'test-secret')
|
||||
self.assertIsNone(request.get_header('Xc-token' if authorization == '1' else 'Authorization'))
|
||||
|
||||
def test_missing_invalid_no_network(self):
|
||||
for env in ({}, {'XC_TOKEN': 'placeholder'}, {'XC_TOKEN_FILE': '/nonexistent/token'}):
|
||||
with patch.dict(os.environ, env, clear=True), patch('urllib.request.urlopen') as call:
|
||||
with self.assertRaisesRegex(BuildConfigError, 'missing/invalid credential'):
|
||||
build_config('gpu')
|
||||
with patch('sys.argv', ['build_config_dry']), patch('sys.stderr', io.StringIO()):
|
||||
self.assertEqual(main(), 1)
|
||||
call.assert_not_called()
|
||||
|
||||
def test_platform_and_data_errors(self):
|
||||
for body, reason in [({'code': 40100}, '40100'), ({'code': 40101}, '40101'), ({'code': 9}, 'other HTTP/platform'), ({'code': 0, 'data': ''}, 'empty/non-str'), ({'code': 0, 'data': []}, 'empty/non-str'), ({'code': 0, 'data': 'test-secret'}, 'credential echoed')]:
|
||||
with patch.dict(os.environ, {'XC_TOKEN': 'test-secret'}, clear=True), patch('urllib.request.urlopen', return_value=self.response(body)):
|
||||
with self.assertRaisesRegex(BuildConfigError, reason):
|
||||
build_config('gpu')
|
||||
|
||||
def test_http_transport_invalid_response(self):
|
||||
for code in (40100, 40101):
|
||||
error = urllib.error.HTTPError('https://example.test', 403, '', {}, io.BytesIO(json.dumps({'code': code}).encode()))
|
||||
with patch.dict(os.environ, {'XC_TOKEN': 'test-secret'}, clear=True), patch('urllib.request.urlopen', side_effect=error):
|
||||
with self.assertRaisesRegex(BuildConfigError, str(code)):
|
||||
build_config('gpu')
|
||||
with patch.dict(os.environ, {'XC_TOKEN': 'test-secret'}, clear=True):
|
||||
for error in (TimeoutError('secret'), urllib.error.URLError('secret')):
|
||||
with patch('urllib.request.urlopen', side_effect=error), self.assertRaisesRegex(BuildConfigError, 'transport/timeout'):
|
||||
build_config('gpu')
|
||||
for response in (self.response([], 200), self.response({'code': 0, 'data': 'yaml'}, 500)):
|
||||
with patch('urllib.request.urlopen', return_value=response), self.assertRaises(BuildConfigError):
|
||||
build_config('gpu')
|
||||
response = self.response({})
|
||||
response.read.return_value = b'not-json'
|
||||
with patch('urllib.request.urlopen', return_value=response), self.assertRaisesRegex(BuildConfigError, 'invalid JSON'):
|
||||
build_config('gpu')
|
||||
|
||||
def test_import_no_io(self):
|
||||
import src.build_config_client
|
||||
with patch('urllib.request.urlopen') as call, patch('pathlib.Path.read_text') as read:
|
||||
runpy.run_path('src/build_config_client.py')
|
||||
call.assert_not_called()
|
||||
read.assert_not_called()
|
||||
47
tests/test_config_params.py
Normal file
47
tests/test_config_params.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import json
|
||||
import re
|
||||
import unittest
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from pathlib import Path
|
||||
from src.config_params import build_config_params, config_params_source, GAP
|
||||
from src.build_config_client import BuildConfigError
|
||||
from scripts.dry_run import _render
|
||||
|
||||
|
||||
class TestConfigParams(unittest.TestCase):
|
||||
def setUp(self):
|
||||
env = patch.dict(os.environ, {}, clear=True)
|
||||
env.start()
|
||||
self.addCleanup(env.stop)
|
||||
|
||||
def test_source_selection_and_failure(self):
|
||||
with patch("urllib.request.urlopen") as network:
|
||||
self.assertEqual(config_params_source(), "sample")
|
||||
build_config_params()
|
||||
network.assert_not_called()
|
||||
self.assertIn("discovered via capture", GAP)
|
||||
with patch.dict(os.environ, {"CONFIG_PARAMS_SOURCE": "build-config"}):
|
||||
with self.assertRaisesRegex(BuildConfigError, "missing/invalid credential"):
|
||||
build_config_params("gpu")
|
||||
with patch.dict(os.environ, {"ALLOW_BUILD_CONFIG": "1", "XC_TOKEN": "test-secret"}), patch("src.config_params.build_config", return_value="api: yaml") as client:
|
||||
self.assertEqual(config_params_source(), "build-config")
|
||||
self.assertEqual(build_config_params("gpu"), "api: yaml")
|
||||
client.assert_called_once_with("gpu", "vllm", "text-generation")
|
||||
with patch.dict(os.environ, {"CONFIG_PARAMS_SOURCE": "build-config"}), patch("src.config_params.build_config", side_effect=BuildConfigError("transport/timeout")):
|
||||
with self.assertRaisesRegex(BuildConfigError, "transport/timeout"):
|
||||
build_config_params("gpu")
|
||||
|
||||
def test_official_sample_exact(self):
|
||||
scrape = Path('/workspace/modelhub-adapt/logs/api-docs-scrape-2026-10-16.md')
|
||||
if scrape.is_file():
|
||||
body = json.loads(re.search(r'```json\n(.*?)\n```', scrape.read_text(), re.S)[1])
|
||||
self.assertEqual(build_config_params(), body['configParams'])
|
||||
self.assertIn('max_model_len: 4096', build_config_params())
|
||||
self.assertIn('framework: vllm', build_config_params())
|
||||
self.assertIn('NOT production-ready', GAP)
|
||||
|
||||
def test_report_draft_and_gap(self):
|
||||
report = _render([{'model_address': 'org/a', 'try_next_gpus': ['壁砺']}], 'test', 'fixture')
|
||||
for value in ('代提候选 / 需 Victor 确认', 'configParams source', 'configParams_source=sample', 'GAP:', 'truncated', 'max_model_len: 4096', 'perf_order=unset', 'order_source=experience'):
|
||||
self.assertIn(value, report)
|
||||
@@ -19,6 +19,27 @@ class FakeClient:
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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 _clear_gate_env(self):
|
||||
return mock.patch.dict(
|
||||
os.environ,
|
||||
@@ -78,7 +99,7 @@ class TestGate(unittest.TestCase):
|
||||
self.assertEqual(d.mode, "live")
|
||||
|
||||
def test_open_gate_without_client_still_blocked(self) -> None:
|
||||
"""Phase1: no real HTTP client even when gate is open."""
|
||||
"""Phase2: an open gate never constructs a network client."""
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
|
||||
49
tests/test_pending_pool.py
Normal file
49
tests/test_pending_pool.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from src.pending_pool import build_ordered_candidates, parse_pending_txt
|
||||
|
||||
|
||||
class TestPendingPool(unittest.TestCase):
|
||||
def test_order_filters_and_first_gpu(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
md, txt, dnr, banned = [root / n for n in ('pending.md', 'pending.txt', 'dnr.md', 'banned.md')]
|
||||
md.write_text('''## Section B
|
||||
| 1 | `org/cold` | 50 | | |
|
||||
## Section A
|
||||
| 1 | `org/good` | 曦望 | 壁砺, 天垓150 | — |
|
||||
| 2 | `org/exact` | 曦望 | 壁砺 | — |
|
||||
| 3 | `org/Heretic-model` | 曦望 | 壁砺 | — |
|
||||
| 4 | `org/OLMo-2-model` | 曦望 | 壁砺 | — |
|
||||
''')
|
||||
dnr.write_text('| 1 | `org/exact` | fail |\n## Uniqueness collisions\n- `org/collision`\n## Overnight\n- `org/overnight`')
|
||||
banned.write_text('`abliterated` `heretic` `uncensored` `unsensored`')
|
||||
rows = build_ordered_candidates(md, txt, dnr, banned)
|
||||
self.assertEqual([r['section'] for r in rows], ['A'] * 4 + ['B'])
|
||||
self.assertEqual(rows[0]['try_next_gpus'][0], '壁砺')
|
||||
self.assertEqual(rows[1]['skip_reason'], 'do-not-resubmit')
|
||||
self.assertEqual(rows[2]['status'], 'skipped')
|
||||
self.assertIn('family', rows[3]['skip_reason'])
|
||||
self.assertTrue(rows[-1]['needs_human'])
|
||||
txt.write_text('org/b\t#B cold\norg/a\t#A done:曦望; still try:壁砺+天垓150\norg/c\\t#A try:天垓150\n')
|
||||
self.assertEqual(parse_pending_txt(txt)[1]['try_next_gpus'], ['壁砺', '天垓150'])
|
||||
fallback = build_ordered_candidates(root/'absent', txt, dnr, banned)
|
||||
self.assertEqual([r['model_address'] for r in fallback], ['org/a', 'org/c', 'org/b'])
|
||||
self.assertEqual(len(build_ordered_candidates(md, txt, dnr, banned)), 5)
|
||||
|
||||
def test_experience_order_missing_only_and_ties(self):
|
||||
from src.pending_pool import experience_order, parse_pending_md
|
||||
self.assertEqual(experience_order(['智铠 mrv-100', '天垓100', '曦望', '天垓150', '壁砺']),
|
||||
['曦望', '壁砺', '天垓150', '天垓100', '智铠 mrv-100'])
|
||||
self.assertEqual(experience_order(['天垓150']), ['天垓150'])
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / 'pending.md'
|
||||
path.write_text('## Section A\n| 1 | `org/a` | 壁砺:验证失败; 曦望:正在验证 | 天垓150 | no 壁砺 retry |\n## Section B\n| 1 | `org/b` | 0 | 壁砺 |\n')
|
||||
from unittest.mock import patch
|
||||
import os
|
||||
with patch.dict(os.environ, {'PENDING_SECTION': 'A'}):
|
||||
rows = build_ordered_candidates(path, dnr_path=Path(directory)/'none', banned_path=Path(directory)/'none')
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]['try_next_gpus'], ['天垓150'])
|
||||
self.assertEqual(rows[0]['perf_order'], 'unset')
|
||||
61
tests/test_readonly_probe.py
Normal file
61
tests/test_readonly_probe.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
import urllib.error
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.readonly_probe import probe_readonly
|
||||
from scripts.probe_readonly import main
|
||||
|
||||
|
||||
class TestReadonlyProbe(unittest.TestCase):
|
||||
def test_missing_token_no_network(self):
|
||||
with patch.dict(os.environ, {}, clear=True), patch('urllib.request.urlopen') as call:
|
||||
self.assertFalse(probe_readonly()['auth_ok'])
|
||||
with patch('sys.stdout', io.StringIO()):
|
||||
self.assertEqual(main(), 1)
|
||||
call.assert_not_called()
|
||||
|
||||
def test_success_and_exact_request(self):
|
||||
response = MagicMock()
|
||||
response.__enter__.return_value = response
|
||||
response.status = 200
|
||||
response.read.return_value = json.dumps({'code': 0, 'data': {
|
||||
'records': [{'modelId': 'secret-value'}], 'total': 10}}).encode()
|
||||
with patch.dict(os.environ, {'EXTERNAL_SERVICE_TOKEN': 'secret-value'}, clear=True), patch('urllib.request.urlopen', return_value=response) as call:
|
||||
result = probe_readonly()
|
||||
request = call.call_args.args[0]
|
||||
self.assertEqual(request.get_method(), 'GET')
|
||||
self.assertEqual(request.full_url, 'https://modelhub.org.cn/api/adapt/task/page?onlyMine=true&pageSize=5¤t=1')
|
||||
self.assertEqual(request.get_header('Xc-token'), 'secret-value')
|
||||
self.assertTrue(result['auth_ok'])
|
||||
self.assertEqual(result['record_summary'], {'count': 1, 'total': 10})
|
||||
self.assertNotIn('secret-value', json.dumps(result))
|
||||
|
||||
def test_errors_no_fallback_or_echo(self):
|
||||
for code in (40100, 40101, 50000):
|
||||
error = urllib.error.HTTPError('https://example.test', 403, '', {}, io.BytesIO(json.dumps({'code': code, 'message': 'secret-value'}).encode()))
|
||||
with patch.dict(os.environ, {'XC_TOKEN': 'secret-value'}, clear=True), patch('urllib.request.urlopen', side_effect=error) as call:
|
||||
result = probe_readonly()
|
||||
self.assertEqual(call.call_count, 1)
|
||||
self.assertFalse(result['auth_ok'])
|
||||
self.assertEqual(result['platform_code'], code)
|
||||
self.assertIn(str(code) if code != 50000 else 'other', result['error_attribution'])
|
||||
self.assertNotIn('secret-value', json.dumps(result))
|
||||
|
||||
def test_transport_and_invalid_json(self):
|
||||
for error in (OSError('secret-value'), urllib.error.URLError('secret-value')):
|
||||
with patch.dict(os.environ, {'XC_TOKEN': 'secret-value'}, clear=True), patch('urllib.request.urlopen', side_effect=error):
|
||||
self.assertEqual(probe_readonly()['error_attribution'], 'transport/timeout')
|
||||
response = MagicMock()
|
||||
response.__enter__.return_value = response
|
||||
response.status, response.read.return_value = 200, b'invalid secret-value'
|
||||
with patch.dict(os.environ, {'XC_TOKEN': 'secret-value'}, clear=True), patch('urllib.request.urlopen', return_value=response):
|
||||
self.assertFalse(probe_readonly()['auth_ok'])
|
||||
|
||||
def test_import_no_network(self):
|
||||
import importlib
|
||||
import src.readonly_probe
|
||||
with patch('urllib.request.urlopen') as call:
|
||||
importlib.reload(src.readonly_probe)
|
||||
call.assert_not_called()
|
||||
@@ -21,9 +21,23 @@ class TestReportSafety(unittest.TestCase):
|
||||
}, clear=True):
|
||||
report = _render([], "test-time", Path("example.json"))
|
||||
self.assertNotIn(sentinel, report)
|
||||
for required in ("待对接 pending-submit", "A→B", "需 Victor 人工提交", "积分:待平台核实", "≥95"):
|
||||
for required in ("A→B", "需 Victor 人工提交", "积分:待平台核实", "≥95"):
|
||||
self.assertIn(required, report)
|
||||
|
||||
def test_pending_draft_queue_banner_and_skips(self):
|
||||
rows = [dict(model_address='org/a', section='A', try_next_gpus=['壁砺', '天垓150'],
|
||||
status='skipped', skip_reason='do-not-resubmit', needs_human=False),
|
||||
dict(model_address='org/b', section='B', try_next_gpus=[],
|
||||
status='needs_human', needs_human=True)]
|
||||
with mock.patch.dict(os.environ, {'ASYNC_QUEUE_DEPTH': '95'}, clear=True):
|
||||
report = _render(rows, 'test', Path('pending.md'))
|
||||
self.assertIn('禁 live / 队列≥95', report)
|
||||
self.assertIn('do-not-resubmit', report)
|
||||
self.assertIn('needs_human', report)
|
||||
self.assertIn('"targetGpu": "壁砺"', report)
|
||||
self.assertNotIn('"targetGpu": "天垓150"', report)
|
||||
self.assertLess(report.index('Section A'), report.index('Section B'))
|
||||
|
||||
def test_http_log_omits_request_details(self):
|
||||
output = io.StringIO()
|
||||
with contextlib.redirect_stderr(output):
|
||||
|
||||
69
tests/test_task_add_client.py
Normal file
69
tests/test_task_add_client.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import importlib
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
from src import task_add_client as module
|
||||
|
||||
|
||||
class TestTaskAddClient(unittest.TestCase):
|
||||
def setUp(self):
|
||||
patch = mock.patch.dict(os.environ, {'STRATEGY_ID': 'sid', 'XC_TOKEN': 'test-secret-credential'}, clear=True)
|
||||
patch.start()
|
||||
self.addCleanup(patch.stop)
|
||||
self.payload = module.build_payload('org/model', '壁砺', 'vllm', config_params={ 'max_model_len': 4096 })
|
||||
|
||||
def test_import_no_network(self):
|
||||
with mock.patch('urllib.request.urlopen') as urlopen:
|
||||
importlib.reload(module)
|
||||
urlopen.assert_not_called()
|
||||
|
||||
def test_success(self):
|
||||
response = mock.MagicMock()
|
||||
response.__enter__.return_value = response
|
||||
response.status = 200
|
||||
response.read.return_value = b'{"code":0,"message":"test-secret-credential"}'
|
||||
with mock.patch('urllib.request.urlopen', return_value=response) as urlopen:
|
||||
result = module.TaskAddClient().submit(self.payload)
|
||||
self.assertEqual(result['http_status'], 200)
|
||||
self.assertNotIn(os.environ['XC_TOKEN'], str(result))
|
||||
request = urlopen.call_args.args[0]
|
||||
self.assertEqual(request.get_header('Xc-token'), os.environ['XC_TOKEN'])
|
||||
self.assertEqual(request.method, 'POST')
|
||||
urlopen.assert_called_once()
|
||||
|
||||
def test_http_4xx_60014(self):
|
||||
error = urllib.error.HTTPError('https://example.invalid', 403, 'forbidden', {}, io.BytesIO(b'{"code":60014}'))
|
||||
with mock.patch('urllib.request.urlopen', side_effect=error) as urlopen:
|
||||
result = module.TaskAddClient().submit(self.payload)
|
||||
self.assertEqual(result['http_status'], 403)
|
||||
self.assertEqual(result['platform_code'], 60014)
|
||||
self.assertIn('D (unverified)', result['error_attribution'])
|
||||
urlopen.assert_called_once()
|
||||
|
||||
def test_transport_no_retry(self):
|
||||
with mock.patch('urllib.request.urlopen', side_effect=TimeoutError('test-secret-credential')) as urlopen:
|
||||
result = module.TaskAddClient().submit(self.payload)
|
||||
self.assertNotIn(os.environ['XC_TOKEN'], str(result))
|
||||
self.assertEqual(result['error_attribution'], 'transport/timeout')
|
||||
urlopen.assert_called_once()
|
||||
|
||||
def test_token_precedence_and_rejections(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/'credential'
|
||||
path.write_text(' file-credential \n')
|
||||
with mock.patch.dict(os.environ, {'XC_TOKEN_FILE': str(path)}):
|
||||
self.assertEqual(module.resolve_token(), 'file-credential')
|
||||
for token in ('', 'tmp', 'PLACEHOLDER', 'changeme', 'tmp-token'):
|
||||
with self.subTest(token=token), mock.patch.dict(os.environ, {'XC_TOKEN': token}):
|
||||
with self.assertRaises(ValueError):
|
||||
module.resolve_token()
|
||||
|
||||
def test_placeholder_never_requests(self):
|
||||
with mock.patch('urllib.request.urlopen') as urlopen:
|
||||
with self.assertRaises(ValueError):
|
||||
module.TaskAddClient().submit(module.build_payload('org/model'))
|
||||
urlopen.assert_not_called()
|
||||
Reference in New Issue
Block a user