test(regression): add selectable resource lifecycle cases (#2794)

This commit is contained in:
huanghuoguoguo
2026-10-28 16:11:19 +08:00
committed by GitHub
parent f837083781
commit 77dfa17c57
10 changed files with 851 additions and 14 deletions
+3 -2
View File
@@ -173,7 +173,7 @@
{"id": "auth.revocation",
"area": "auth", "kind": "blackbox", "priority": "P0",
"description": "Revoked credentials stop authorizing new requests",
"bindings": []
"bindings": [{"profile": "scenarios", "case": "auth_revocation", "checks": ["credential_active", "credential_revoked", "key_status", "key_cleanup"]}]
},
{"id": "workspace.read_isolation",
"area": "workspace", "kind": "blackbox", "priority": "P0",
@@ -248,7 +248,7 @@
{"id": "agent.direct_execution",
"area": "agent", "kind": "blackbox", "priority": "P1",
"description": "Standalone Agent entrypoint completes through the public API",
"bindings": []
"bindings": [{"profile": "scenarios", "case": "agent_direct_execution", "checks": ["agent_response", "agent_terminal", "agent_events", "agent_cleanup"]}]
},
{"id": "persistence.postgres_parity",
"area": "persistence", "kind": "blackbox", "priority": "P1",
@@ -265,6 +265,7 @@
"description": "A real embedding model retrieves relevant fixed documents",
"bindings": []
},
{"id": "retrieval.delete_visibility", "area": "retrieval", "kind": "blackbox", "priority": "P1", "description": "Deleting one ingested document removes its retrieval results while preserving a second document", "bindings": [{"profile": "scenarios", "case": "retrieval_delete_visibility", "checks": ["retrieval_initial", "retrieval_deleted", "retrieval_control", "document_cleanup"]}]},
{"id": "execution.persisted_terminal",
"area": "execution", "kind": "graybox", "priority": "P0",
"description": "Each executed request has a unique completed persisted run and terminal event",
+105
View File
@@ -0,0 +1,105 @@
"""Optional local scenarios share the Suite's verified Workspace and credentials."""
from contextlib import closing
import importlib
import json
from pathlib import Path
import sqlite3
import time
import urllib.error
import urllib.request
GROUPS = {'resources': 'regression_resource_cases'}
class ScenarioUnavailable(RuntimeError):
"""An explicit prerequisite is missing; this is not a passed assertion."""
def require(condition, message):
if not condition:
raise ValueError(message)
def endpoint(suite, path):
require(path.startswith('/') and not path.startswith('//'), 'Scenario API path must be relative to Core')
base = suite.connection.get('endpoint')
require(bool(base), 'Scenario needs the endpoint verified by lbctl whoami')
return base.rstrip('/') + path
def auth_headers(suite):
source = suite.connection.get('credential_source', '')
key = suite.env.get(source.removeprefix('env:')) if source.startswith('env:') else None
require(bool(key), 'Scenario requires environment-backed lbctl credentials')
if key not in suite.secrets:
suite.secrets.append(key)
return {'Authorization': 'Bearer ' + key, 'X-Workspace-Id': suite.variables['workspace_uuid']}
headers = auth_headers
def api(suite, method, path, body=None, headers=None):
request_headers = dict(auth_headers(suite) if headers is None else headers)
request_headers.setdefault('X-Workspace-Id', suite.variables['workspace_uuid'])
if body is not None:
request_headers['Content-Type'] = 'application/json'
request = urllib.request.Request(
endpoint(suite, path),
method=method,
headers=request_headers,
data=json.dumps(body).encode() if body is not None else None,
)
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
try:
response = opener.open(request, timeout=suite.timeout(90))
except urllib.error.HTTPError as exc:
response = exc
with response:
return response.status, json.load(response)
def record_run(suite, row, run_id, expected_status):
"""Register exact run ownership for the independent final ledger check."""
require(expected_status in {'completed', 'failed', 'cancelled', 'timeout'}, 'Expected Run status must be terminal')
path = Path(suite.path(suite.profile['smoke']['ledger_db'])).resolve()
deadline = time.monotonic() + suite.timeout(30)
with closing(sqlite3.connect(path.as_uri() + '?mode=ro', uri=True, timeout=5)) as connection:
connection.row_factory = sqlite3.Row
while True:
run = connection.execute(
'SELECT run_id, conversation_id, status, finished_at FROM agent_run '
'WHERE workspace_id=? AND run_id=? AND id>?',
(suite.variables['workspace_uuid'], run_id, row['after_id']),
).fetchone()
if run and run['finished_at']:
observed = dict(run)
observed.update(expected_status=expected_status, after_id=row['after_id'])
row.setdefault('scenario_runs', []).append(observed)
require(run['status'] == expected_status, f'Run ended as {run["status"]}; expected {expected_status}')
return observed
if time.monotonic() >= deadline:
raise TimeoutError('Scenario Run did not reach a persisted terminal state')
time.sleep(0.1)
def modules():
return {name: importlib.import_module(module) for name, module in GROUPS.items()}
def add_cases(profile, groups):
groups = list(groups)
require(len(groups) == len(set(groups)), 'Scenario groups must not repeat')
require(not (set(groups) - GROUPS.keys()), 'Unknown local scenario group')
for name in groups:
importlib.import_module(GROUPS[name]).add_cases(profile)
profile['scenario_groups'] = groups
def run_case(suite, case, row):
for name, module in modules().items():
if case['scenario'] in module.CASE_NAMES:
require(name in suite.profile.get('scenario_groups', []), f'Scenario group is disabled: {name}')
return module.run_case(suite, case, row)
raise ValueError(f'Unknown local scenario: {case["scenario"]}')
+342
View File
@@ -0,0 +1,342 @@
"""Optional resource lifecycle scenarios against the authenticated Workspace APIs."""
import copy
import json
from pathlib import Path
import sys
import tempfile
from uuid import uuid4
from regression_coverage import checked
from regression_local_cases import ScenarioUnavailable, api, require
from regression_suite import resolve
GROUP = 'resources'
CASE_NAMES = {'auth_revocation', 'agent_direct_execution', 'retrieval_delete_visibility'}
def add_cases(profile):
resources = {item['id']: item for item in profile['resources']}
base = copy.deepcopy(resources['empty_knowledge_base'])
base.update(id='resource_knowledge_base')
base['body']['name'] = 'regression document lifecycle'
base.pop('documents', None)
profile['resources'].append(base)
for name, stage, expected in (
('auth_revocation', 'auth', 'LANGBOT_AUTH_REVOCATION_OK'),
('agent_direct_execution', 'agent', 'LANGBOT_CHAT_OK'),
('retrieval_delete_visibility', 'retrieval', 'LANGBOT_DOCUMENT_DELETE_OK'),
):
profile['cases'].append({'name': name, 'scenario': name, 'stage': stage, '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})',
)
return response.get('data', {})
def journal(suite, row):
row.setdefault('resource_journal', f'resource-case-{uuid4().hex}.json')
suite.write(
row['resource_journal'],
{
'workspace_uuid': suite.variables['workspace_uuid'],
'resources': row.get('owned_resources', []),
'cleanup_errors': row.get('cleanup_errors', []),
},
)
def cleanup(suite, row, assertion, action):
original = sys.exc_info()[1]
try:
checked(row, assertion, action)
except Exception as exc:
message = f'{type(exc).__name__}: {exc}'
for secret in suite.secrets:
message = message.replace(secret, '[REDACTED]')
row.setdefault('cleanup_errors', []).append(message)
if original is None:
raise
finally:
journal(suite, row)
def auth_revocation(suite, case, row):
token = suite.env.get('LANGBOT_QA_USER_TOKEN')
if not token:
raise ScenarioUnavailable('auth.revocation requires LANGBOT_QA_USER_TOKEN with api_key.manage')
if token not in suite.secrets:
suite.secrets.append(token)
manager = {'Authorization': 'Bearer ' + token}
name = 'qa-revoke-' + uuid4().hex
owned = {'kind': 'api-key', 'name': name, 'state': 'creating'}
row.setdefault('owned_resources', []).append(owned)
journal(suite, row)
def revoke_owned():
if 'id' not in owned:
keys = data(suite, 'GET', '/api/v1/apikeys', headers=manager)['keys']
matches = [key for key in keys if key.get('name') == name]
require(len(matches) == 1, 'Created key identity is unresolved; inspect the resource journal')
owned['id'] = matches[0]['id']
journal(suite, row)
path = f'/api/v1/apikeys/{owned["id"]}'
key = data(suite, 'GET', path, headers=manager)['key']
if key['status'] != 'revoked':
data(suite, 'DELETE', path, headers=manager)
key = data(suite, 'GET', path, headers=manager)['key']
require(key['status'] == 'revoked', 'Owned credential remains active after cleanup')
owned['state'] = 'revoked'
try:
created = data(suite, 'POST', '/api/v1/apikeys', {'name': name, 'scopes': ['resource.view']}, headers=manager)[
'key'
]
owned.update(id=created['id'], uuid=created['uuid'], state='active')
journal(suite, row)
secret = created['key']
require(isinstance(secret, str) and bool(secret), 'Key creation did not return its one-time secret')
suite.secrets.append(secret)
child = {'X-API-Key': secret}
identity = data(suite, 'GET', '/api/v1/system/context', headers=child)
checked(
row,
'credential_active',
lambda: require(
identity.get('workspace_uuid') == suite.variables['workspace_uuid']
and identity.get('api_key_id') == owned['uuid'],
'New credential did not authenticate as its own Workspace key',
),
)
data(suite, 'GET', '/api/v1/pipelines', headers=child)
data(suite, 'DELETE', f'/api/v1/apikeys/{owned["id"]}', headers=manager)
status, response = api(suite, 'GET', '/api/v1/pipelines', headers=child)
checked(
row,
'credential_revoked',
lambda: require(
status == 401 and isinstance(response, dict) and response.get('code') != 0,
'Revoked credential still authorizes resource access',
),
)
key = data(suite, 'GET', f'/api/v1/apikeys/{owned["id"]}', headers=manager)['key']
checked(
row,
'key_status',
lambda: require(
key.get('status') == 'revoked' and key.get('secret_available') is False and 'key' not in key,
'Revoked key readback has incorrect status or exposes its secret',
),
)
return {'result': case['expect'], 'key_id': owned['id'], 'status': 'revoked'}
finally:
cleanup(suite, row, 'key_cleanup', revoke_owned)
def agent_direct_execution(suite, case, row):
template = next(item for item in suite.profile['resources'] if item['id'] == 'chat_pipeline')
config = resolve(copy.deepcopy(template['body']['config']['ai']), suite.variables)
name = 'qa-agent-' + uuid4().hex
conversation = 'qa-direct-' + uuid4().hex
owned = {'kind': 'agent', 'name': name, 'state': 'creating'}
row.setdefault('owned_resources', []).append(owned)
journal(suite, row)
def delete_owned():
if 'uuid' not in owned:
agents = data(suite, 'GET', '/api/v1/agents')['agents']
matches = [agent for agent in agents if agent.get('name') == name and agent.get('kind') == 'agent']
require(len(matches) == 1, 'Created Agent identity is unresolved; inspect the resource journal')
owned['uuid'] = matches[0]['uuid']
journal(suite, row)
path = f'/api/v1/agents/{owned["uuid"]}'
status, _ = api(suite, 'GET', path)
if status != 404:
require(status == 200, 'Cannot read owned Agent before cleanup')
data(suite, 'DELETE', path)
status, _ = api(suite, 'GET', path)
require(status == 404, 'Owned Agent remains visible after cleanup')
owned['state'] = 'deleted'
try:
created = data(suite, 'POST', '/api/v1/agents', {'name': name, 'kind': 'agent', 'config': config})
owned.update(uuid=created['uuid'], state='created')
journal(suite, row)
path = f'/api/v1/agents/{owned["uuid"]}'
reply = data(
suite, 'POST', path + '/debug', {'text': 'Run the direct Agent scenario', 'conversation_id': conversation}
)
runs = data(suite, 'GET', path + '/runs')['items']
matches = [run for run in runs if run.get('conversation_id') == conversation]
require(len(matches) == 1, 'Direct Agent request did not produce exactly one matching Run')
run = matches[0]
row.setdefault('scenario_runs', []).append(
{
'run_id': run['run_id'],
'conversation_id': conversation,
'expected_status': 'completed',
'after_id': row['after_id'],
}
)
checked(
row,
'agent_terminal',
lambda: require(
run.get('status') == 'completed' and bool(run.get('finished_at')),
'Direct Agent Run did not reach completed status',
),
)
checked(
row,
'agent_response',
lambda: require(
reply.get('conversation_id') == conversation and case['expect'] in reply.get('final_text', ''),
'Direct Agent returned the wrong conversation or business reply',
),
)
events = data(suite, 'GET', path + f'/runs/{run["run_id"]}/events')
checked(
row,
'agent_events',
lambda: require(
events.get('run', {}).get('run_id') == run['run_id']
and any(
event.get('type') == 'run.completed' and event.get('run_id') == run['run_id']
for event in events.get('items', [])
),
'Direct Agent completion event is missing',
),
)
return {'result': reply['final_text'], 'run_id': run['run_id'], 'conversation_id': conversation}
finally:
cleanup(suite, row, 'agent_cleanup', delete_owned)
def retrieval_delete_visibility(suite, case, row):
kb_uuid = suite.variables['resource_knowledge_base']
path = f'/api/v1/knowledge/bases/{kb_uuid}'
before = {item['uuid'] for item in data(suite, 'GET', path + '/files')['files']}
owned = {'kind': 'knowledge-files', 'knowledge_base_uuid': kb_uuid, 'files': [], 'state': 'ingesting'}
row.setdefault('owned_resources', []).append(owned)
journal(suite, row)
def files():
return [item for item in data(suite, 'GET', path + '/files')['files'] if item['uuid'] not in before]
def cleanup_documents():
remaining = files()
owned['files'] = [{'uuid': item['uuid'], 'status': item['status']} for item in remaining]
journal(suite, row)
unresolved = [item['uuid'] for item in remaining if item['status'] in {'pending', 'processing', 'interrupted'}]
for item in remaining:
if item['uuid'] not in unresolved:
data(suite, 'DELETE', path + '/files/' + item['uuid'])
require(
not unresolved, 'Document ingestion outcome is unresolved; retain the owned knowledge base for inspection'
)
require(not files(), 'Owned documents remain visible after cleanup')
owned['state'] = 'deleted'
def retrieve(query):
found = data(suite, 'POST', path + '/retrieve', {'query': query, 'retrieval_settings': {'top_k': 10}})[
'results'
]
require(
isinstance(found, list)
and all(
isinstance(item, dict)
and isinstance(item.get('metadata'), dict)
and isinstance(item.get('content'), list)
for item in found
),
'Malformed retrieval response',
)
return found
def contains(found, document, marker):
return any(
item.get('metadata', {}).get('file_id') == document['engine_document_id']
and marker in json.dumps(item.get('content', []), ensure_ascii=False)
for item in found
)
try:
documents, markers = [], ['obsolete' + uuid4().hex, 'retained' + uuid4().hex]
with tempfile.TemporaryDirectory(prefix='resource-documents-', dir=suite.output) as directory:
for index, marker in enumerate(markers):
document = Path(directory) / f'document-{index}.txt'
document.write_text(f'Lifecycle fixture: {marker}. This is document {index}.\n')
previous = {item['uuid'] for item in files()}
suite.call(
[
'knowledge-base',
'ingest',
kb_uuid,
'--file',
str(document),
'--parser-plugin-id',
'langbot-team/GeneralParsers',
'--wait',
]
)
added = [item for item in files() if item['uuid'] not in previous]
require(
len(added) == 1 and added[0].get('status') == 'completed',
'Document ingestion did not create exactly one completed file',
)
documents.append(added[0])
owned['files'].append({'uuid': added[0]['uuid'], 'status': added[0]['status']})
journal(suite, row)
initial = retrieve(' '.join(markers))
checked(
row,
'retrieval_initial',
lambda: require(
all(contains(initial, document, marker) for document, marker in zip(documents, markers)),
'Both fixture documents must be independently retrievable before deletion',
),
)
data(suite, 'DELETE', path + '/files/' + documents[0]['uuid'])
after = retrieve(' '.join(markers))
checked(
row,
'retrieval_deleted',
lambda: require(
all(
item.get('metadata', {}).get('file_id') != documents[0]['engine_document_id']
and markers[0] not in json.dumps(item, ensure_ascii=False)
for item in after
)
and documents[0]['uuid'] not in {item['uuid'] for item in files()},
'Deleted document remains visible in retrieval or the file list',
),
)
checked(
row,
'retrieval_control',
lambda: require(
contains(after, documents[1], markers[1]),
'Deleting one document removed the surviving control document',
),
)
return {
'result': case['expect'],
'deleted_document_id': documents[0]['uuid'],
'control_document_id': documents[1]['uuid'],
}
finally:
cleanup(suite, row, 'document_cleanup', cleanup_documents)
def run_case(suite, case, row):
actions = {
'auth_revocation': auth_revocation,
'agent_direct_execution': agent_direct_execution,
'retrieval_delete_visibility': retrieval_delete_visibility,
}
return actions[case['scenario']](suite, case, row)
+5 -1
View File
@@ -11,7 +11,7 @@ ROOT = Path(__file__).parent
RUNNER = 'plugin:langbot-team/LocalAgent/default'
def expanded_profile():
def expanded_profile(groups=()):
profile = json.loads((ROOT / 'regression_profiles/smoke.json').read_text())
profile['coverage_suite'] = 'scenarios'
profile['smoke']['group_turns'] = 3
@@ -102,6 +102,10 @@ def expanded_profile():
expect='LANGBOT_' + name.upper() + '_OK',
)
profile['cases'].append(case)
if groups:
from regression_local_cases import add_cases
add_cases(profile, groups)
return profile
+34 -7
View File
@@ -16,6 +16,7 @@ from uuid import uuid4
from regression_traffic import group_request, percentile, read_endpoint_snapshot, reconcile_runs
from regression_coverage import checked, check_response
from regression_local_cases import ScenarioUnavailable
class UnresolvedRun(RuntimeError):
@@ -316,7 +317,11 @@ def run_cycle(suite, rows, health, cycle):
token = uuid4().hex
offset = mock_path.stat().st_size if case.get('mock_requests') and mock_path.exists() else 0
try:
if rejected:
if 'scenario' in case:
from regression_local_cases import run_case
data = run_case(suite, case, row)
elif rejected:
data = checked(
row,
'response',
@@ -358,7 +363,7 @@ def run_cycle(suite, rows, health, cycle):
row['output'] = data
if case.get('forbid'):
checked(row, 'isolation', lambda: check_response(data, case['expect'], case['forbid']))
row['checks'] = {}
row.setdefault('checks', {})
if case.get('fields'):
row['checks']['fields'] = checked(row, 'fields', lambda: check_fields(data, case['fields']))
if case.get('attachment'):
@@ -392,6 +397,8 @@ def run_cycle(suite, rows, health, cycle):
row['status'] = 'passed'
previous[group] = token
history_turns[group] += 1
except ScenarioUnavailable as exc:
row.update(status='skipped', error=redact(str(exc)))
except urllib.error.HTTPError as exc:
row.update(
status='unknown' if exc.code >= 500 else 'failed', error=f'HTTP {exc.code}; not retried'
@@ -413,6 +420,8 @@ def run_cycle(suite, rows, health, cycle):
if 'timed out' in message:
row['timed_out'] = True
row['seconds'] = time.monotonic() - before
for error in row.get('cleanup_errors', []):
errors.append(f'{case["name"]}: cleanup failed: {redact(str(error))}')
if row['status'] != 'passed':
errors.append(f'{case["name"]}: {row["status"]}: {row["error"]}')
if is_group:
@@ -462,7 +471,7 @@ def run_smoke(suite):
}
)
suite.write('cycles.json', cycles)
if not execution or not execution.admitting() or any(row['status'] == 'unknown' for row in rows):
if not execution or not execution.admitting() or any(row['status'] in ('unknown', 'skipped') for row in rows):
break
result['errors'] = errors
result['cycles'] = cycles
@@ -470,7 +479,7 @@ def run_smoke(suite):
result['incomplete'] = [
f'{row["case"]}: {row["status"]}: {row["error"]}'
for row in rows
if row['status'] == 'unknown' and not row.get('timed_out')
if row['status'] in ('unknown', 'skipped') and not row.get('timed_out')
]
if not result['first_pass_complete']:
error = 'Required first pass did not finish before the admission deadline'
@@ -486,7 +495,7 @@ def run_smoke(suite):
for name in dict.fromkeys(row['case'] for row in rows)
},
}
executed = [row for row in rows if row.get('requires_runner') and 'after_id' in row]
executed = scenario_run_rows(rows)
result['test_data'] = {
'observed_conversations': len({row['conversation_id'] for row in executed if 'conversation_id' in row}),
'observed_agent_runs': len({row['run']['run_id'] for row in executed if 'run' in row}),
@@ -497,9 +506,24 @@ def run_smoke(suite):
return result
def scenario_run_rows(rows):
executed = [row for row in rows if row.get('requires_runner') and 'after_id' in row]
for row in rows:
for run in row.get('scenario_runs', []):
executed.append(
{
'after_id': row['after_id'],
'conversation_id': run['conversation_id'],
'run': {'run_id': run['run_id']},
'expected_status': run['expected_status'],
}
)
return executed
def reconcile_scenario_runs(suite, result, *, key='ledger'):
"""Recheck the originally observed conversations without expanding run ownership."""
executed = [row for row in result.get('cases', []) if row.get('requires_runner') and 'after_id' in row]
executed = scenario_run_rows(result.get('cases', []))
if not executed:
return
try:
@@ -509,6 +533,9 @@ def reconcile_scenario_runs(suite, result, *, key='ledger'):
min(row['after_id'] for row in executed),
{row['conversation_id'] for row in executed if 'conversation_id' in row},
{row['run']['run_id'] for row in executed if 'run' in row},
expected_statuses={
row['run']['run_id']: row.get('expected_status', 'completed') for row in executed if 'run' in row
},
)
except (OSError, sqlite3.Error) as exc:
error = f'Scenario {key} evidence unavailable: {exc}'
@@ -516,5 +543,5 @@ def reconcile_scenario_runs(suite, result, *, key='ledger'):
result.setdefault('incomplete', []).append(error)
return
if not result[key]['matched']:
error = f'Scenario {key} found extra, missing or non-completed AgentRuns'
error = f'Scenario {key} found extra, missing or unexpected AgentRun states'
result['errors'].append(error)
+27 -1
View File
@@ -83,6 +83,7 @@ def validate(profile):
'coverage_suite',
'execution',
'performance_mode',
'scenario_groups',
}
if extra := profile.keys() - allowed:
raise ValueError(f'unknown profile fields: {sorted(extra)}')
@@ -98,6 +99,16 @@ def validate(profile):
raise ValueError('coverage_suite must be a stable suite identifier')
if profile.get('performance_mode', 'gate') not in {'gate', 'observe'}:
raise ValueError('performance_mode must be gate or observe')
if 'scenario_groups' in profile:
from regression_local_cases import GROUPS
groups = profile['scenario_groups']
if (
not isinstance(groups, list)
or any(not isinstance(name, str) or name not in GROUPS for name in groups)
or len(groups) != len(set(groups))
):
raise ValueError('scenario_groups must contain unique supported local groups')
execution = profile.get('execution')
if execution is not None:
if (
@@ -192,6 +203,7 @@ def validate(profile):
'reject_group',
'http_probe',
'reset_group',
'scenario',
}:
raise ValueError('unknown case field')
if not isinstance(case.get('name'), str) or not case['name'] or case['name'] in names:
@@ -250,7 +262,21 @@ def validate(profile):
case['reset_group'] is not True or 'http_group' not in case or 'reject_group' in case
):
raise ValueError('reset_group requires an accepted group case')
if 'http_probe' in case:
if 'scenario' in case:
from regression_local_cases import modules
handlers = modules()
if (
'smoke' not in profile
or not isinstance(case['scenario'], str)
or not any(
case['scenario'] in module.CASE_NAMES and name in profile.get('scenario_groups', [])
for name, module in handlers.items()
)
or any(key in case for key in ('args', 'body', 'http_probe', 'http_group', 'box_exec', 'attachment'))
):
raise ValueError('scenario must name a supported finite local case without another request mode')
elif 'http_probe' in case:
probe = case['http_probe']
if (
not isinstance(probe, dict)
+7 -2
View File
@@ -131,9 +131,14 @@ def group_request(case, group_id, member_id, turn, token, previous, env=None, *,
return payload
def reconcile_runs(path, workspace, after_id, conversations, expected_ids):
def reconcile_runs(path, workspace, after_id, conversations, expected_ids, *, expected_statuses=None):
"""Catch delayed extra or nonterminal runs after request-level verification."""
actual, unfinished = set(), []
expected_statuses = expected_statuses or {}
if set(expected_statuses) - set(expected_ids) or any(
status not in {'completed', 'failed', 'cancelled', 'timeout'} for status in expected_statuses.values()
):
raise ValueError('Expected statuses must name owned runs and terminal states')
connection = sqlite3.connect(Path(path).resolve().as_uri() + '?mode=ro', uri=True)
try:
values = sorted(conversations)
@@ -145,7 +150,7 @@ def reconcile_runs(path, workspace, after_id, conversations, expected_ids):
)
for run_id, status in connection.execute(query, [workspace, after_id, *part]):
actual.add(run_id)
if status != 'completed':
if status != expected_statuses.get(run_id, 'completed'):
unfinished.append(run_id)
finally:
connection.close()
+6 -1
View File
@@ -291,8 +291,13 @@ class Routes:
path = Path(__file__).with_name('regression_profiles') / f'{name}.json'
if name == 'scenarios':
from regression_scenarios import expanded_profile
from regression_local_cases import GROUPS
profile = expanded_profile()
profile = expanded_profile(GROUPS)
if 'lifecycle' in GROUPS:
from regression_lifecycle_cases import restart_profile
profile['cases'] += restart_profile(profile)['cases']
else:
profile = json.loads(path.read_text())
profiles[name] = {x['name']: x for x in profile['cases']}
+110
View File
@@ -0,0 +1,110 @@
"""Keep optional cases and expected failures from turning incomplete evidence green."""
import json
from pathlib import Path
import sqlite3
import tempfile
import unittest
from unittest.mock import patch
from regression_local_cases import GROUPS, ScenarioUnavailable
from regression_scenarios import expanded_profile
from regression_smoke import run_smoke
from regression_suite import Suite, validate
from regression_traffic import reconcile_runs
class LocalScenarioTests(unittest.TestCase):
def test_group_switches_keep_base_cases_and_reject_unknown_handlers(self):
base = expanded_profile()
for group in GROUPS:
profile = expanded_profile([group])
validate(profile)
self.assertEqual(profile['scenario_groups'], [group])
self.assertGreater(len(profile['cases']), len(base['cases']))
self.assertEqual(profile['cases'][: len(base['cases'])], base['cases'])
for groups in (['unknown'], ['resources', 'resources']):
with self.assertRaises(ValueError):
expanded_profile(groups)
profile = expanded_profile(['resources'])
profile['scenario_groups'] = []
with self.assertRaises(ValueError):
validate(profile)
profile['scenario_groups'] = ['resources']
profile['cases'][-1]['scenario'] = 'not-a-real-handler'
with self.assertRaises(ValueError):
validate(profile)
def test_missing_prerequisite_is_skipped_and_incomplete(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
with sqlite3.connect(root / 'ledger.db') as connection:
connection.execute('CREATE TABLE agent_run (id INTEGER)')
profile = {
'version': 1,
'cli': {'config': 'context.yaml'},
'smoke': {'ledger_db': 'ledger.db'},
'cases': [{'name': 'auth_revocation', 'scenario': 'auth_revocation', 'expect': 'OK'}],
'observe': {'health': {}},
}
suite = Suite(profile, root / 'profile.json', root)
with patch('regression_local_cases.run_case', side_effect=ScenarioUnavailable('Missing user credential')):
result = run_smoke(suite)
self.assertEqual(result['status'], {'skipped': 1})
self.assertEqual(result['errors'], result['incomplete'])
self.assertIn('Missing user credential', result['incomplete'][0])
self.assertFalse(result['cases'][0].get('assertions'))
self.assertEqual(json.loads((root / 'cases.json').read_text())[0]['status'], 'skipped')
def test_cleanup_failure_remains_fail_after_unknown_request(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
with sqlite3.connect(root / 'ledger.db') as connection:
connection.execute('CREATE TABLE agent_run (id INTEGER)')
profile = {
'version': 1,
'cli': {'config': 'context.yaml'},
'smoke': {'ledger_db': 'ledger.db'},
'cases': [{'name': 'agent_direct_execution', 'scenario': 'agent_direct_execution', 'expect': 'OK'}],
'observe': {'health': {}},
}
suite = Suite(profile, root / 'profile.json', root)
def fail(suite, case, row):
row['cleanup_errors'] = ['Agent deletion returned HTTP 500']
raise RuntimeError('Request outcome unknown')
with patch('regression_local_cases.run_case', side_effect=fail):
result = run_smoke(suite)
suite.finish(result)
self.assertEqual(result['outcome'], 'FAIL')
self.assertEqual(result['cases'][0]['status'], 'unknown')
self.assertTrue(any('cleanup failed' in message for message in result['errors']))
def test_expected_failure_does_not_allow_extra_or_running_runs(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'ledger.db'
with sqlite3.connect(path) as connection:
connection.execute(
'CREATE TABLE agent_run (id INTEGER, run_id TEXT, workspace_id TEXT, conversation_id TEXT, status TEXT)'
)
connection.execute("INSERT INTO agent_run VALUES (1, 'owned', 'ws', 'session', 'failed')")
args = (path, 'ws', 0, {'session'}, {'owned'})
self.assertFalse(reconcile_runs(*args)['matched'])
expected = {'owned': 'failed'}
self.assertTrue(reconcile_runs(*args, expected_statuses=expected)['matched'])
with sqlite3.connect(path) as connection:
connection.execute("UPDATE agent_run SET status='running'")
self.assertFalse(reconcile_runs(*args, expected_statuses=expected)['matched'])
with sqlite3.connect(path) as connection:
connection.execute("UPDATE agent_run SET status='failed'")
connection.execute("INSERT INTO agent_run VALUES (2, 'extra', 'ws', 'session', 'completed')")
result = reconcile_runs(*args, expected_statuses=expected)
self.assertFalse(result['matched'])
self.assertEqual(result['extra'], ['extra'])
with self.assertRaises(ValueError):
reconcile_runs(*args, expected_statuses={'owned': 'running'})
if __name__ == '__main__':
unittest.main()
+212
View File
@@ -0,0 +1,212 @@
"""Challenge public resource lifecycle oracles and their cleanup without starting services."""
import json
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from regression_local_cases import ScenarioUnavailable
from regression_resource_cases import run_case
def success(data=None):
return 200, {'code': 0, 'data': data or {}}
class ResourceCasesTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.suite = SimpleNamespace(
output=self.root,
env={'LANGBOT_QA_USER_TOKEN': 'private-user-token'},
secrets=[],
variables={'workspace_uuid': 'workspace', 'resource_knowledge_base': 'owned-kb'},
profile={
'resources': [
{
'id': 'chat_pipeline',
'body': {'config': {'ai': {'runner': {'id': 'runner'}, 'runner_config': {'runner': {}}}}},
}
]
},
write=lambda name, value: (self.root / name).write_text(json.dumps(value)),
)
def test_missing_management_session_skips_without_creating_credentials(self):
self.suite.env.clear()
with patch('regression_resource_cases.api') as request, self.assertRaises(ScenarioUnavailable):
run_case(self.suite, {'scenario': 'auth_revocation'}, {})
request.assert_not_called()
def test_revocation_rejects_still_authorized_key_and_never_persists_secrets(self):
for stale_authorization in (False, True):
with self.subTest(stale_authorization=stale_authorization):
key = {'id': 7, 'uuid': 'managed-key', 'status': 'active', 'secret_available': False}
row = {'after_id': 0}
def request(suite, method, path, body=None, headers=None):
if method == 'POST':
return success({'key': {**key, 'key': 'private-child-secret', 'secret_available': True}})
if method == 'DELETE':
key['status'] = 'revoked'
return success()
if path == '/api/v1/system/context':
self.assertEqual(headers, {'X-API-Key': 'private-child-secret'})
return success({'workspace_uuid': 'workspace', 'api_key_id': 'managed-key'})
if path == '/api/v1/pipelines':
return (
success({'pipelines': []})
if key['status'] == 'active' or stale_authorization
else (401, {'code': -1})
)
return success({'key': key})
with patch('regression_resource_cases.api', side_effect=request):
case = {'scenario': 'auth_revocation', 'expect': 'REVOKED'}
if stale_authorization:
with self.assertRaisesRegex(ValueError, 'still authorizes'):
run_case(self.suite, case, row)
self.assertEqual(row['assertions']['credential_revoked'], 'failed')
else:
self.assertEqual(run_case(self.suite, case, row)['status'], 'revoked')
self.assertEqual(key['status'], 'revoked')
self.assertEqual(row['assertions']['key_cleanup'], 'passed')
evidence = json.dumps(row) + (self.root / row['resource_journal']).read_text()
self.assertNotIn('private-child-secret', evidence)
self.assertNotIn('private-user-token', evidence)
def test_direct_agent_records_run_and_cleans_after_failed_public_event_oracle(self):
for missing_event in (False, True):
with self.subTest(missing_event=missing_event):
state, row = {}, {'after_id': 12}
def request(suite, method, path, body=None, headers=None):
if method == 'POST' and path == '/api/v1/agents':
state.update(uuid='own-agent', name=body['name'])
return success({'uuid': 'own-agent'})
if path.endswith('/debug'):
state['conversation'] = body['conversation_id']
return success({'conversation_id': state['conversation'], 'final_text': 'DIRECT_OK'})
run = {
'run_id': 'own-run',
'conversation_id': state.get('conversation'),
'status': 'completed',
'finished_at': 1,
}
if path.endswith('/runs'):
return success({'items': [run]})
if path.endswith('/events'):
return success(
{
'run': run,
'items': [] if missing_event else [{'type': 'run.completed', 'run_id': 'own-run'}],
}
)
if method == 'DELETE':
self.assertEqual(path, '/api/v1/agents/own-agent')
state['deleted'] = True
return success()
return (404, {'code': -1}) if state.get('deleted') else success({'agent': state})
with patch('regression_resource_cases.api', side_effect=request):
case = {'scenario': 'agent_direct_execution', 'expect': 'DIRECT_OK'}
if missing_event:
with self.assertRaisesRegex(ValueError, 'completion event'):
run_case(self.suite, case, row)
else:
self.assertEqual(run_case(self.suite, case, row)['run_id'], 'own-run')
self.assertEqual(
row['scenario_runs'],
[
{
'run_id': 'own-run',
'conversation_id': state['conversation'],
'after_id': 12,
'expected_status': 'completed',
}
],
)
self.assertTrue(state['deleted'])
self.assertEqual(row['assertions']['agent_cleanup'], 'passed')
def test_unknown_agent_create_recovers_owned_identity_without_retrying_creation(self):
calls, state, row = [], {}, {'after_id': 0}
def request(suite, method, path, body=None, headers=None):
calls.append((method, path))
if method == 'POST':
state.update(uuid='own-agent', name=body['name'], kind='agent')
raise ConnectionResetError('creation response lost')
if path == '/api/v1/agents':
return success({'agents': [state, {'uuid': 'unrelated', 'name': 'other', 'kind': 'agent'}]})
if method == 'DELETE':
self.assertEqual(path, '/api/v1/agents/own-agent')
state['deleted'] = True
return success()
return (404, {'code': -1}) if state.get('deleted') else success({'agent': state})
with patch('regression_resource_cases.api', side_effect=request), self.assertRaises(ConnectionResetError):
run_case(self.suite, {'scenario': 'agent_direct_execution', 'expect': 'OK'}, row)
self.assertEqual(sum(method == 'POST' for method, _ in calls), 1)
self.assertEqual(row['owned_resources'][0]['state'], 'deleted')
def test_delete_requires_retrieval_absence_and_preserves_control_document(self):
for stale_vectors in (False, True):
with self.subTest(stale_vectors=stale_vectors):
documents, row = [], {'after_id': 0}
def ingest(args):
content = Path(args[args.index('--file') + 1]).read_text()
index = len(documents)
documents.append(
{
'uuid': f'file-{index}',
'engine_document_id': f'engine-{index}',
'status': 'completed',
'content': content,
'deleted': False,
}
)
return {}
def request(suite, method, path, body=None, headers=None):
if method == 'DELETE':
target = next(document for document in documents if path.endswith('/' + document['uuid']))
target['deleted'] = True
return success()
if method == 'GET':
return success({'files': [document for document in documents if not document['deleted']]})
return success(
{
'results': [
{
'metadata': {'file_id': document['engine_document_id']},
'content': [{'type': 'text', 'text': document['content']}],
}
for document in documents
if not document['deleted'] or stale_vectors
]
}
)
self.suite.call = ingest
with patch('regression_resource_cases.api', side_effect=request):
case = {'scenario': 'retrieval_delete_visibility', 'expect': 'DELETED'}
if stale_vectors:
with self.assertRaisesRegex(ValueError, 'remains visible'):
run_case(self.suite, case, row)
self.assertEqual(row['assertions']['retrieval_deleted'], 'failed')
else:
self.assertEqual(run_case(self.suite, case, row)['result'], 'DELETED')
self.assertEqual(row['assertions']['retrieval_control'], 'passed')
self.assertTrue(all(document['deleted'] for document in documents))
self.assertEqual(row['assertions']['document_cleanup'], 'passed')
self.assertEqual(list(self.root.glob('resource-documents-*')), [])
if __name__ == '__main__':
unittest.main()