WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user