#!/usr/bin/env python3 """Verify migrated assets through acceptance HTTP without printing bodies.""" import argparse import json import secrets import urllib.error import urllib.request import uuid from pathlib import Path from apply_weknora_assets import target_snapshot from apply_weknora_identities import sql_run from weknora_asset_etl import LEDGER, asset_id from weknora_category_etl import literal, save_json from weknora_identity_etl import TARGET_DATABASE, TARGET_TENANT def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--credentials', type=Path, required=True) parser.add_argument('--out', type=Path, required=True) args = parser.parse_args() if args.out.exists(): parser.error('Report exists') opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) checks, fixture_user, fixture_agent = [], None, None completed = False def request(path, token=None, data=None, method=None, tenant=TARGET_TENANT): headers = {'Content-Type': 'application/json'} if token: headers.update(Authorization='Bearer ' + token) if tenant: headers['X-Tenant-ID'] = str(tenant) req = urllib.request.Request('http://127.0.0.1:18080' + path, headers=headers, data=json.dumps(data).encode() if data is not None else None, method=method) try: with opener.open(req, timeout=30) as response: return response.status, json.load(response) except urllib.error.HTTPError as error: return error.code, {} def check(name, passed): checks.append({'check': name, 'passed': bool(passed)}) before = target_snapshot() if before['database'] != TARGET_DATABASE: raise RuntimeError('Acceptance database mismatch') credentials = json.loads(args.credentials.read_text()) status, auth = request('/api/v1/auth/login', data={k: credentials[k] for k in ('email', 'password')}) if status != 200 or not auth.get('token'): raise RuntimeError('Acceptance administrator login failed') token = auth['token'] expected = json.loads(sql_run(f"SELECT json_agg(json_build_object('kind',source_kind,'source_id',source_id,'id',target_id,'native',native_snapshot)) FROM {LEDGER} WHERE target_tenant_id={TARGET_TENANT} AND source_kind IN ('agent','skill');")) agents = [a for a in expected if a['kind'] == 'agent'] skills = [a for a in expected if a['kind'] == 'skill'] try: status, response = request('/api/v1/agents', token) listed = {a['id']: a for a in response.get('data', [])} check('administrator_sees_all_15_agents', status == 200 and len(agents) == 15 and all(a['id'] in listed for a in agents)) for item in agents: status, response = request('/api/v1/agents/' + item['id'], token) native, current = item['native'], response.get('data', {}) check('agent_' + str(item['source_id']) + '_body_model_bindings_and_owner', status == 200 and all(current.get(k) == native[k] for k in ('name', 'created_by', 'visibility', 'enabled')) and all(current.get('config', {}).get(k, []) == native['config'][k] for k in ('system_prompt', 'model_id', 'knowledge_bases', 'instruction_skill_ids'))) status, response = request('/api/v1/skills/instructions', token) listed = {a['id']: a for a in response.get('data', [])} check('administrator_sees_all_9_skills', status == 200 and len(skills) == 9 and all(s['id'] in listed for s in skills)) for item in skills: status, response = request('/api/v1/skills/instructions/' + item['id'], token) current, native = response.get('data', {}), item['native'] check('skill_' + str(item['source_id']) + '_body_and_owner', status == 200 and all(current.get(k) == native[k] for k in ('name', 'instructions', 'created_by', 'visibility', 'enabled'))) for endpoint in ('/api/v1/agents', '/api/v1/skills/instructions'): status, _ = request(endpoint) check('anonymous_' + endpoint.rsplit('/', 1)[-1] + '_denied', status in (401, 403)) suffix = secrets.token_hex(6) password = secrets.token_urlsafe(18) + 'aA1!' email = 'asset-check-' + suffix + '@migration.invalid' status, response = request('/api/v1/system/admin/users/create', token, {'username': 'asset-check-' + suffix, 'email': email, 'password': password}) fixture_user = response.get('user', {}).get('id') if status != 201 or not fixture_user or str(uuid.UUID(fixture_user)) != fixture_user: raise RuntimeError(f'Temporary acceptance user provisioning failed (HTTP {status})') sql_run(f"INSERT INTO tenant_members(user_id,tenant_id,role,status) VALUES ({literal(fixture_user)},{TARGET_TENANT},'contributor','active');") status, member_auth = request('/api/v1/auth/login', data={'email': email, 'password': password}) if status != 200 or not member_auth.get('token'): raise RuntimeError('Temporary acceptance user login failed') member_token = member_auth['token'] status, response = request('/api/v1/agents', member_token) visible = {a['id'] for a in response.get('data', [])} check('member_sees_13_global_agents_and_no_private_agents', status == 200 and all((a['id'] in visible) == (a['native']['visibility'] == 'global' and a['native']['enabled']) for a in agents)) status, response = request('/api/v1/skills/instructions', member_token) visible = {a['id'] for a in response.get('data', [])} check('member_sees_7_global_skills_and_no_private_skills', status == 200 and all((s['id'] in visible) == (s['native']['visibility'] == 'global') for s in skills)) for kind, number, endpoint in (('agent', 22, '/api/v1/agents/'), ('skill', 5, '/api/v1/skills/instructions/'), ('skill', 9, '/api/v1/skills/instructions/')): status, _ = request(endpoint + asset_id(kind, number), member_token) check('private_' + kind + '_' + str(number) + '_direct_read_denied', status in (403, 404)) # A valid no-op payload avoids modifying the original even if a guard regresses. first = agents[0] status, _ = request('/api/v1/agents/' + first['id'], member_token, {'name': first['native']['name'], 'config': first['native']['config']}, 'PUT') check('member_cannot_update_global_original_agent', status in (403, 404)) status, _ = request('/api/v1/skills/instructions/mount', member_token, {'agent_ids': [first['id']], 'skill_ids': [asset_id('skill', 1)], 'mode': 'add'}) check('member_cannot_mount_on_another_owners_agent', status in (403, 404)) status, response = request('/api/v1/agents', member_token, { 'name': 'asset-check-' + suffix, 'config': {'agent_mode': 'quick-answer', 'kb_selection_mode': 'none', 'skills_selection_mode': 'none', 'model_id': first['native']['config']['model_id']}}) fixture_agent = response.get('data', {}).get('id') if status != 201 or not fixture_agent or str(uuid.UUID(fixture_agent)) != fixture_agent: raise RuntimeError('Temporary Agent creation failed') status, response = request('/api/v1/skills/instructions/mount', member_token, {'agent_ids': [fixture_agent], 'skill_ids': [asset_id('skill', 1)], 'mode': 'add'}) check('member_can_mount_global_skill_on_own_agent', status == 200) status, response = request('/api/v1/agents/' + fixture_agent, member_token) check('mounted_global_skill_persisted', status == 200 and asset_id('skill', 1) in response.get('data', {}).get('config', {}).get('instruction_skill_ids', [])) status, _ = request('/api/v1/skills/instructions/mount', member_token, {'agent_ids': [fixture_agent], 'skill_ids': [asset_id('skill', 5)], 'mode': 'add'}) check('member_cannot_mount_private_skill', status in (403, 404)) status, _ = request('/api/v1/agents/' + first['id'], member_token, tenant=10060) check('unrelated_tenant_header_denied', status in (403, 404)) completed = True finally: if fixture_user: # Scope cleanup by the exact user returned by this run, never by name prefixes. sql_run(f"BEGIN; DELETE FROM custom_agents WHERE created_by={literal(fixture_user)}; DELETE FROM tenant_members WHERE user_id={literal(fixture_user)} AND tenant_id={TARGET_TENANT}; DELETE FROM auth_tokens WHERE user_id={literal(fixture_user)}; UPDATE users SET is_active=false,updated_at=now() WHERE id={literal(fixture_user)}; COMMIT;") check('existing_assets_models_and_knowledge_unchanged', before == target_snapshot()) check('all_api_checks_completed', completed) result = {'passed': all(c['passed'] for c in checks), 'checks': checks, 'fixture_user_deactivated': bool(fixture_user), 'fixture_agent_removed': bool(fixture_agent)} save_json(args.out, result) print(json.dumps(result)) return 0 if result['passed'] else 1 if __name__ == '__main__': raise SystemExit(main())