Files
LangBot/tools/test_regression_job.py
T
huanghuoguoguo 5ec41f15a1 test(ci): add a 60-second core smoke gate and regression jobs (#2711)
* test(tools): add deterministic lbctl regression job

* test(tools): simulate persistent multi-group conversations

* test(tools): add finite core smoke with terminal run and file oracles

* test(tools): expand smoke coverage across execution stages

* ci: add a timed real core smoke and lightweight tool checks

* fix(ci): keep infrastructure failures visible in smoke verdicts
2026-10-23 16:24:26 +08:00

196 lines
8.1 KiB
Python

"""Exercise Job ownership and lifecycle with controlled CLI/Docker responses."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import shutil
import sys
import tempfile
import threading
import unittest
from unittest.mock import patch
import yaml
from regression_job import Job, main
from test_regression_suite import FAKE_CLI, profile
class ControlledJob(Job):
def __init__(self, manifest, suite, path, output, health, fail_install=False):
super().__init__(manifest, suite, path, output)
self.health = health
self.fail_install = fail_install
self.calls = []
self.env['LBCTL_TEST_ROOT'] = str(output.parent)
def command(self, args, timeout=900):
self.calls.append(args)
if args[: len(self.cli)] == self.cli:
operation = args[len(self.cli) :]
if operation[0] == 'install':
self.deploy.mkdir()
(self.deploy / 'data').mkdir()
(self.deploy / 'compose.yaml').write_text('services: {}')
(self.deploy / '.env').write_text(
'LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN=' + 't' * 32 + '\nLANGBOT_BOX_ENABLED=true\n'
)
record = {
'deployment_id': 'owned-deployment',
'compose_project': 'test-project',
'compose_file': str(self.deploy / 'compose.yaml'),
'data_dir': str(self.deploy / 'data'),
'env_file': str(self.deploy / '.env'),
'endpoint': self.health,
}
(self.deploy / 'deployment.yaml').write_text(yaml.safe_dump(record))
# Simulate a timeout/failed final probe with no returned record.
return (
(1, json.dumps({'data': {}}), 'install failed')
if self.fail_install
else (0, json.dumps({'data': {'record': record}}), '')
)
if operation[0] == '--endpoint':
return 0, json.dumps({'data': {'workspace_uuid': 'test-workspace'}}), ''
if operation[0] == 'uninstall':
shutil.rmtree(self.deploy / 'data')
return 0, json.dumps({'data': {}}), ''
if args[:2] == ['docker', 'ps']:
if 'label=langbot.box=true' in args:
return 0, '', ''
return (
0,
'\n'.join(
f'id-{name} {name}'
for name in ('langbot', 'langbot_plugin_runtime', 'langbot_box', 'regression_mock')
),
'',
)
if args[:2] == ['docker', 'inspect']:
if '{{.State.Pid}}' in args:
return 0, str(os.getpid()), ''
return 0, json.dumps({'network': {'IPAddress': '127.0.0.1'}}), ''
if args[:2] == ['docker', 'compose']:
if 'logs' in args:
return 0, self.redact(self.env.get('LANGBOT_QA_API_KEY', '') + '\n' + 't' * 32), ''
return 0, '', ''
raise AssertionError(args)
@staticmethod
def ready(url):
pass
def prepare(self):
suite = super().prepare()
# Real traffic driver and HTTP health probe; infrastructure calls are controlled above.
suite.profile['observe'] = {'health': {'core': self.health}}
return suite
class JobTests(unittest.TestCase):
def test_lifecycle_success_and_partial_install_recovery(self):
class Health(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b'{"code":0}')
def log_message(self, *args):
pass
server = ThreadingHTTPServer(('127.0.0.1', 0), Health)
thread = threading.Thread(target=server.serve_forever)
thread.start()
try:
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
cli = root / 'lbctl-test'
cli.write_text(f'#!{sys.executable}\n' + FAKE_CLI)
cli.chmod(0o700)
manifest = {'lbctl': str(cli), 'deployment': {'version': 'v4.11.0', 'port': 15351}}
suite = profile()
suite['resources'] = [{'id': 'chat', 'kind': 'pipeline', 'body': {'name': 'QA', 'config': {'ai': {}}}}]
suite['cases'][0].update(args=['pipeline', 'run', '${chat}'], expect='TEST_OK')
suite['phases'][0].update(duration_seconds=0.5, rps=4, workers=2)
health = f'http://127.0.0.1:{server.server_port}'
job = ControlledJob(manifest, suite, root / 'profile.json', root / 'success', health)
result = job.run()
self.assertTrue(result['passed'], result)
self.assertEqual(result['phase'], 'cleaned')
self.assertFalse((job.deploy / 'data').exists())
self.assertFalse((job.private / 'api-key').exists())
self.assertIn('--purge-data', job.calls[-1])
self.assertNotIn('t' * 32, (job.output / 'services.log').read_text())
job = ControlledJob(manifest, suite, root / 'profile.json', root / 'failed', health, fail_install=True)
result = job.run()
self.assertFalse(result['passed'])
self.assertEqual(job.state['record']['deployment_id'], 'owned-deployment')
self.assertTrue((job.deploy / 'data').exists())
self.assertFalse(any('uninstall' in call for call in job.calls))
with (
patch.object(sys, 'argv', ['regression_job.py', 'cleanup', '--output', str(job.output)]),
patch('regression_job.Job', return_value=job),
):
main()
self.assertFalse((job.deploy / 'data').exists())
self.assertEqual(job.state['phase'], 'cleaned')
finally:
server.shutdown()
server.server_close()
thread.join()
def test_cleanup_refuses_changed_deployment(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
job = Job({}, profile(), root / 'profile.json', root)
job.private.mkdir()
job.deploy.mkdir()
(job.private / 'job-id').write_text(job.state['job_id'])
record = {
'deployment_id': 'old',
'compose_file': str(job.deploy / 'compose.yaml'),
'data_dir': str(job.deploy / 'data'),
'env_file': str(job.deploy / '.env'),
}
job.state['record'] = record
(job.deploy / 'deployment.yaml').write_text(yaml.safe_dump({**record, 'deployment_id': 'new'}))
with self.assertRaisesRegex(RuntimeError, 'identity changed'):
job.cleanup()
def test_sandbox_cleanup_uses_owned_mount_roots(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
job = Job({}, profile(), root / 'profile.json', root)
job.state['record'] = {'data_dir': str(job.deploy / 'data')}
containers = ['ours', 'other']
def checked(args, timeout=900):
if args[1] == 'ps':
return '\n'.join(containers)
return '\n'.join(
json.dumps(
{
'Id': container,
'Mounts': [
{
'Source': str(job.deploy / 'data/box/tenant')
if container != 'other'
else '/unrelated/box/tenant'
}
],
}
)
for container in args[4:]
)
job.checked = checked
self.assertEqual(job.owned_sandboxes(), ['ours'])
containers.append('later')
self.assertEqual(job.owned_sandboxes(), ['ours', 'later'])
if __name__ == '__main__':
unittest.main()