mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-01 13:25:18 +08:00
Reorganize regression tools into explicit packages and update all repository references.
249 lines
11 KiB
Python
249 lines
11 KiB
Python
"""Local plugin business checks through signed Bot ingress and the Page API."""
|
|
|
|
from contextlib import closing
|
|
import copy
|
|
import json
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
from tools.entrypoints.regression_coverage import checked
|
|
from tools.regression.regression_lifecycle_cases import ledger
|
|
from tools.regression.regression_local_cases import api, require
|
|
from tools.regression.regression_resource_cases import cleanup, journal
|
|
from tools.regression.regression_smoke import check_mock_requests, signed_headers
|
|
from tools.entrypoints.regression_suite import resolve
|
|
|
|
GROUP = 'plugins'
|
|
CASE_NAMES = {'plugins.command_version', 'plugins.faq_crud'}
|
|
COMMAND_PLUGINS = [
|
|
{'author': 'langbot-team', 'name': name}
|
|
for name in ('LocalAgent', 'LangRAG', 'GeneralParsers', 'EssentialCommands')
|
|
]
|
|
FAQ_API = '/api/v1/plugins/RockChinQ/FAQManager/page-api'
|
|
|
|
|
|
def add_cases(profile):
|
|
pipeline = copy.deepcopy(next(item for item in profile['resources'] if item['id'] == 'chat_pipeline'))
|
|
pipeline['id'] = 'plugin_command_pipeline'
|
|
pipeline['body']['name'] = 'regression plugin commands'
|
|
pipeline['body']['extensions_preferences'] = {
|
|
'enable_all_plugins': False,
|
|
'plugins': copy.deepcopy(COMMAND_PLUGINS),
|
|
}
|
|
profile['resources'].append(pipeline)
|
|
for name, expected in (
|
|
('plugins.command_version', 'LANGBOT_PLUGIN_COMMAND_OK'),
|
|
('plugins.faq_crud', 'LANGBOT_PLUGIN_FAQ_OK'),
|
|
):
|
|
profile['cases'].append({'name': name, 'scenario': name, 'stage': 'plugins', 'expect': expected})
|
|
|
|
|
|
def data(suite, method, path, body=None, *, headers=None):
|
|
status, response = api(suite, method, path, body, headers=headers)
|
|
require(
|
|
status == 200 and isinstance(response, dict) and response.get('code') == 0,
|
|
f'{method} {path} did not return a successful response (HTTP {status})',
|
|
)
|
|
result = response.get('data')
|
|
if method == 'DELETE' and result is None:
|
|
return {}
|
|
require(isinstance(result, dict), f'{method} {path} returned malformed data')
|
|
return result
|
|
|
|
|
|
def command_version(suite, case, row):
|
|
pipeline = data(suite, 'GET', f'/api/v1/pipelines/{suite.variables["plugin_command_pipeline"]}')['pipeline']
|
|
preferences = pipeline.get('extensions_preferences', {})
|
|
checked(
|
|
row,
|
|
'command_plugin_bindings',
|
|
lambda: require(
|
|
preferences.get('enable_all_plugins') is False and preferences.get('plugins') == COMMAND_PLUGINS,
|
|
'Command pipeline must bind only the three base plugins and EssentialCommands',
|
|
),
|
|
)
|
|
version = data(suite, 'GET', '/api/v1/system/info').get('version')
|
|
require(isinstance(version, str) and bool(version), 'Core did not report its version')
|
|
records = Path(suite.path(suite.profile['smoke']['mock_records']))
|
|
require(records.is_file(), 'Command model-call evidence requires the dedicated mock recording file')
|
|
body = resolve(
|
|
copy.deepcopy(next(item['body'] for item in suite.profile['resources'] if item['id'] == 'chat_bot')),
|
|
suite.variables,
|
|
)
|
|
body['name'] = 'qa-plugin-command-' + uuid4().hex
|
|
body['adapter_config']['default_session_type'] = 'person'
|
|
body['event_bindings'] = [
|
|
{
|
|
'event_pattern': 'message.received',
|
|
'target_type': 'pipeline',
|
|
'target_uuid': suite.variables['plugin_command_pipeline'],
|
|
'enabled': True,
|
|
'filters': [],
|
|
}
|
|
]
|
|
body['plugin_processors'] = []
|
|
owned = {'kind': 'bot', 'name': body['name'], 'state': 'creating'}
|
|
row.setdefault('owned_resources', []).append(owned)
|
|
journal(suite, row)
|
|
|
|
def delete_bot():
|
|
if 'uuid' not in owned:
|
|
bots = data(suite, 'GET', '/api/v1/platform/bots')['bots']
|
|
matches = [bot for bot in bots if bot.get('name') == owned['name']]
|
|
require(len(matches) == 1, 'Created command Bot identity is unresolved; inspect the resource journal')
|
|
owned['uuid'] = matches[0]['uuid']
|
|
journal(suite, row)
|
|
path = f'/api/v1/platform/bots/{owned["uuid"]}'
|
|
status, response = api(suite, 'GET', path)
|
|
if status != 404:
|
|
require(
|
|
status == 200
|
|
and response.get('code') == 0
|
|
and response.get('data', {}).get('bot', {}).get('name') == owned['name'],
|
|
'Cannot confirm ownership of the command Bot before cleanup',
|
|
)
|
|
data(suite, 'DELETE', path)
|
|
status, _ = api(suite, 'GET', path)
|
|
require(status == 404, 'Command Bot remains visible after cleanup')
|
|
owned['state'] = 'deleted'
|
|
|
|
try:
|
|
created = data(suite, 'POST', '/api/v1/platform/bots', body)
|
|
owned.update(uuid=created['uuid'], state='created')
|
|
journal(suite, row)
|
|
session = 'qa-plugin-version-' + uuid4().hex
|
|
payload = {
|
|
'session_id': session,
|
|
'session_type': 'person',
|
|
'message': [{'type': 'Plain', 'text': '!version'}],
|
|
}
|
|
offset = records.stat().st_size
|
|
reply = data(
|
|
suite,
|
|
'POST',
|
|
f'/bots/{owned["uuid"]}/sync',
|
|
payload,
|
|
headers=signed_headers(json.dumps(payload).encode(), body['adapter_config']['inbound_secret']),
|
|
)
|
|
row['command_response'] = reply
|
|
parts = reply.get('message')
|
|
checked(
|
|
row,
|
|
'command_version',
|
|
lambda: require(
|
|
reply.get('session_id') == session
|
|
and bool(reply.get('reply_to'))
|
|
and isinstance(parts, list)
|
|
and all(isinstance(part, dict) and part.get('type') == 'Plain' for part in parts)
|
|
and ''.join(part.get('text', '') for part in parts) == f'[bot] LangBot Version: {version}',
|
|
'Version command did not return the exact Core version in the originating session',
|
|
),
|
|
)
|
|
with closing(ledger(suite)) as connection:
|
|
count = connection.execute(
|
|
'SELECT COUNT(*) FROM agent_run WHERE workspace_id=? AND bot_id=? AND id>?',
|
|
(suite.variables['workspace_uuid'], owned['uuid'], row['after_id']),
|
|
).fetchone()[0]
|
|
checked(row, 'command_no_run', lambda: require(count == 0, 'Version command unexpectedly created an AgentRun'))
|
|
checked(
|
|
row,
|
|
'command_no_model',
|
|
lambda: check_mock_requests(records, offset, [], timeout=suite.timeout(3)),
|
|
)
|
|
return {'result': case['expect'], 'version': version, 'agent_runs': count, 'model_requests': 0}
|
|
finally:
|
|
cleanup(suite, row, 'command_bot_cleanup', delete_bot)
|
|
|
|
|
|
def faq_crud(suite, case, row):
|
|
def page(method, endpoint, body=None):
|
|
return data(
|
|
suite, 'POST', FAQ_API, {'page_id': 'manager', 'endpoint': endpoint, 'method': method, 'body': body}
|
|
)
|
|
|
|
def entries():
|
|
values = page('GET', '/entries').get('entries')
|
|
require(
|
|
isinstance(values, list) and all(isinstance(item, dict) and item.get('id') for item in values),
|
|
'FAQ entries response is malformed',
|
|
)
|
|
return values
|
|
|
|
previous = {item['id'] for item in entries()}
|
|
marker = 'qa-faq-' + uuid4().hex
|
|
question, before, after = marker + '-question', marker + '-before', marker + '-after'
|
|
owned = {'kind': 'faq-entry', 'question': question, 'state': 'creating'}
|
|
row.setdefault('owned_resources', []).append(owned)
|
|
journal(suite, row)
|
|
|
|
def expect_entry(values, answer):
|
|
expected = {'id': owned['id'], 'question': question, 'answer': answer}
|
|
require(values == [expected], 'FAQ content or entry identity differs from the submitted value')
|
|
|
|
def delete_entry():
|
|
current = entries()
|
|
if 'id' not in owned:
|
|
matches = [item for item in current if item.get('question') == question and item['id'] not in previous]
|
|
require(len(matches) <= 1, 'Created FAQ identity is ambiguous; inspect the resource journal')
|
|
if not matches:
|
|
owned['state'] = 'absent'
|
|
return
|
|
owned['id'] = matches[0]['id']
|
|
journal(suite, row)
|
|
require(owned['id'] not in previous, 'Refusing to delete a pre-existing FAQ entry')
|
|
matches = [item for item in current if item['id'] == owned['id']]
|
|
if matches:
|
|
require(
|
|
len(matches) == 1 and matches[0].get('question') == question, 'FAQ ownership changed before cleanup'
|
|
)
|
|
deleted = page('DELETE', '/entries', {'id': owned['id']})
|
|
require(deleted.get('deleted') == owned['id'], 'FAQ deletion returned a different entry ID')
|
|
require(not any(item['id'] == owned['id'] for item in entries()), 'Deleted FAQ remains visible')
|
|
require(page('POST', '/search', {'query': marker}).get('results') == [], 'Deleted FAQ remains searchable')
|
|
owned['state'] = 'deleted'
|
|
|
|
try:
|
|
created = page('POST', '/entries', {'question': question, 'answer': before}).get('entry')
|
|
require(isinstance(created, dict) and bool(created.get('id')), 'FAQ creation did not return an entry ID')
|
|
owned.update(id=created['id'], state='created')
|
|
journal(suite, row)
|
|
require(owned['id'] not in previous, 'FAQ creation reused a pre-existing entry ID')
|
|
checked(row, 'faq_create', lambda: expect_entry([created], before))
|
|
checked(
|
|
row, 'faq_read', lambda: expect_entry([item for item in entries() if item['id'] == owned['id']], before)
|
|
)
|
|
checked(
|
|
row, 'faq_search', lambda: expect_entry(page('POST', '/search', {'query': before}).get('results'), before)
|
|
)
|
|
updated = page('PUT', '/entries', {'id': owned['id'], 'question': question, 'answer': after}).get('entry')
|
|
checked(row, 'faq_update', lambda: expect_entry([updated], after))
|
|
checked(
|
|
row,
|
|
'faq_update_read',
|
|
lambda: expect_entry([item for item in entries() if item['id'] == owned['id']], after),
|
|
)
|
|
checked(
|
|
row,
|
|
'faq_update_search',
|
|
lambda: expect_entry(page('POST', '/search', {'query': after}).get('results'), after),
|
|
)
|
|
checked(
|
|
row,
|
|
'faq_old_answer_absent',
|
|
lambda: require(
|
|
page('POST', '/search', {'query': before}).get('results') == [], 'FAQ search returned stale content'
|
|
),
|
|
)
|
|
checked(row, 'faq_delete', delete_entry)
|
|
return {'result': case['expect'], 'entry_id': owned['id'], 'deleted': True}
|
|
finally:
|
|
if owned['state'] != 'deleted':
|
|
cleanup(suite, row, 'faq_cleanup', delete_entry)
|
|
else:
|
|
journal(suite, row)
|
|
|
|
|
|
def run_case(suite, case, row):
|
|
require(case['scenario'] in CASE_NAMES, 'Unknown plugin scenario')
|
|
return (command_version if case['scenario'] == 'plugins.command_version' else faq_crud)(suite, case, row)
|