Files
kefu/wechat_rpa/test_knowledge_review.py
T
2026-09-21 10:34:06 +08:00

268 lines
16 KiB
Python

"""Full-scope review snapshot, permissions, transactions and pagination."""
import json
from unittest import TestCase, mock
import test_knowledge as fixtures
from archive_store import ArchiveStore, new_id
from knowledge_review import KnowledgeReviewStore, KnowledgeReviewWorker
class FullReviewTest(TestCase):
setUp = fixtures.KnowledgeTest.setUp
tearDown = fixtures.KnowledgeTest.tearDown
payload = fixtures.KnowledgeTest.payload
start_job = fixtures.KnowledgeTest.start_job
process = fixtures.KnowledgeTest.process
first = fixtures.KnowledgeTest.first
def seed(self, count=75, tenant='default'):
self.process(tenant)
original = self.first(tenant)
original['conditions'] = '仅用于虚构门店测试'
original = self.store.save(tenant, original['id'], original, self.actor, '')
ids = [original['id']]
with self.db.connect() as db:
for index in range(count - 1):
item_id = new_id(); ids.append(item_id)
db.execute("""INSERT INTO knowledge_item(id,tenant_id,job_id,dedup_hash,title,question,answer,conditions,
category,kind,flags_json,status,revision,valid_until,created_at,updated_at)
SELECT ?,tenant_id,job_id,?,title||?,question,answer,conditions,category,kind,flags_json,
status,revision,valid_until,created_at,updated_at FROM knowledge_item WHERE id=?""",
(item_id, item_id, str(index), original['id']))
db.execute('INSERT INTO knowledge_source SELECT ?,message_id,version_no,role,content,sent_at '
'FROM knowledge_source WHERE item_id=?', (item_id, original['id']))
return ids
def test_all_pages_approve_without_publishing_or_model_calls(self):
self.seed(237)
reviews = KnowledgeReviewStore(self.store)
preview = reviews.preview('*', -1, self.actor, '')
self.assertEqual((preview['total'], preview['preview']['eligible']), (237, 237))
self.assertEqual(len(self.store.list_items('default')['items']), 30)
self.assertEqual(self.store.stats('default').get('approved', 0), 0)
with self.assertRaises(ValueError):
reviews.action(preview['id'], 'start', self.actor, '', False)
reviews.action(preview['id'], 'start', self.actor, '', True)
reviews.action(preview['id'], 'start', self.actor, '', True)
worker = KnowledgeReviewWorker(self.store)
with mock.patch('model_gateway.call_outlet', side_effect=AssertionError('No model call allowed')):
worker.step()
self.assertEqual(reviews.get(preview['id'], self.actor)['processed'], 100)
worker.step(); worker.step()
result = reviews.get(preview['id'], self.actor)
self.assertEqual((result['status'], result['approved'], result['skipped'], result['progress_percent']), ('completed', 237, 0, 100))
self.assertEqual(self.store.stats('default'), {'approved': 237})
with self.db.connect() as db:
self.assertEqual(db.execute('SELECT COUNT(*) FROM knowledge_fts').fetchone()[0], 0)
self.assertEqual(db.execute("SELECT COUNT(*) FROM knowledge_revision WHERE action='approve'").fetchone()[0], 237)
results = reviews.results(preview['id'], self.actor, '*', 'approved', 8, 30)
self.assertEqual((results['total'], len(results['items'])), (237, 27))
def test_preview_reports_missing_conditions_fields_expiry_and_sources(self):
ids = self.seed(6)
with self.db.connect() as db:
db.execute("UPDATE knowledge_item SET conditions='' WHERE id=?", (ids[0],))
db.execute("UPDATE knowledge_item SET question='' WHERE id=?", (ids[1],))
db.execute("UPDATE knowledge_item SET valid_until='2000-01-01' WHERE id=?", (ids[2],))
db.execute('DELETE FROM knowledge_source WHERE item_id=?', (ids[3],))
db.execute('UPDATE knowledge_source SET version_no=99 WHERE item_id=?', (ids[4],))
reviews = KnowledgeReviewStore(self.store)
job = reviews.preview('default', 0, self.actor, '')
self.assertEqual((job['total'], job['preview']['eligible'], job['preview']['blocked']), (6, 1, 5))
self.assertEqual({reason['code'] for reason in job['preview']['reasons']},
{'missing_conditions', 'invalid_fields', 'expired', 'missing_source', 'changed_source'})
reviews.action(job['id'], 'start', self.actor, '', True)
KnowledgeReviewWorker(self.store).step()
final = reviews.get(job['id'], self.actor)
self.assertEqual((final['approved'], final['skipped']), (1, 5))
self.assertEqual(self.store.stats('default'), {'approved': 1, 'draft': 5})
def test_changed_deleted_and_new_items_are_not_approved_from_old_snapshot(self):
ids = self.seed(4)
reviews = KnowledgeReviewStore(self.store)
job = reviews.preview('default', 0, self.actor, '')
edit = self.store.detail('default', ids[0]); edit['answer'] = '已经编辑的新答案,需要重新审核。'
self.store.save('default', ids[0], edit, self.actor, '')
self.store.transition('default', ids[1], 2, 'reject', self.actor, '')
self.store.delete_rejected('default', ids[1], 2, self.actor, '')
with self.db.connect() as db:
db.execute("UPDATE knowledge_item SET status='rejected' WHERE id=?", (ids[2],))
self.seed(1, 'new-tenant')
reviews.action(job['id'], 'start', self.actor, '', True)
KnowledgeReviewWorker(self.store).step()
final = reviews.get(job['id'], self.actor)
self.assertEqual((final['total'], final['approved'], final['skipped']), (4, 1, 3))
self.assertEqual(self.store.stats('new-tenant'), {'draft': 1})
reasons = {row['reason'] for row in reviews.results(job['id'], self.actor, '*')['items']}
self.assertEqual(reasons, {'changed_item', 'deleted'})
def test_single_account_scope_and_snapshot_excludes_later_drafts(self):
self.seed(33); self.seed(4, 'other')
reviews = KnowledgeReviewStore(self.store)
job = reviews.preview('default', 0, self.actor, '')
self.seed(2, 'later')
reviews.action(job['id'], 'start', self.actor, '', True)
KnowledgeReviewWorker(self.store).step()
self.assertEqual(self.store.stats('default'), {'approved': 33})
self.assertEqual(self.store.stats('other'), {'draft': 4})
self.assertEqual(self.store.stats('later'), {'draft': 2})
def test_pause_resume_cancel_and_new_worker_are_durable(self):
self.seed(7)
reviews = KnowledgeReviewStore(self.store)
job = reviews.preview('*', -1, self.actor, '')
reviews.action(job['id'], 'start', self.actor, '', True)
KnowledgeReviewWorker(self.store).step(2)
reviews.action(job['id'], 'pause', self.actor, '')
self.assertFalse(KnowledgeReviewWorker(self.store).step())
reviews.action(job['id'], 'resume', self.actor, '')
KnowledgeReviewWorker(self.store).step(2)
reviews.action(job['id'], 'cancel', self.actor, '')
self.assertFalse(KnowledgeReviewWorker(self.store).step())
result = reviews.get(job['id'], self.actor)
self.assertEqual((result['status'], result['approved'], result['processed']), ('cancelled', 4, 4))
self.assertEqual(self.store.stats('default'), {'approved': 4, 'draft': 3})
with self.assertRaises(ValueError):
reviews.action(job['id'], 'resume', self.actor, '')
def test_batch_failure_rolls_back_status_results_and_audit_together(self):
self.seed(4)
reviews = KnowledgeReviewStore(self.store); job = reviews.preview('*', -1, self.actor, '')
reviews.action(job['id'], 'start', self.actor, '', True)
original = self.store._record; calls = []
def broken(*args, **kwargs):
calls.append(1)
if len(calls) == 2:
raise RuntimeError('Simulated crash')
return original(*args, **kwargs)
with mock.patch.object(self.store, '_record', side_effect=broken), self.assertRaises(RuntimeError):
KnowledgeReviewWorker(self.store).step()
self.assertEqual(self.store.stats('default'), {'draft': 4})
self.assertEqual(reviews.get(job['id'], self.actor)['processed'], 0)
KnowledgeReviewWorker(self.store).step()
self.assertEqual(reviews.get(job['id'], self.actor)['approved'], 4)
def test_reviewer_permission_revoked_mid_job_stops_further_approvals(self):
self.seed(5)
reviews = KnowledgeReviewStore(self.store); job = reviews.preview('*', -1, self.actor, '')
reviews.action(job['id'], 'start', self.actor, '', True)
KnowledgeReviewWorker(self.store).step(2)
with self.db.connect() as db:
db.execute("DELETE FROM role_permissions WHERE role_code='admin' AND permission_code='knowledge:review'")
KnowledgeReviewWorker(self.store).step()
result = reviews.get(job['id'], self.actor)
self.assertEqual((result['status'], result['approved']), ('failed', 2))
with self.db.connect() as db:
db.execute("INSERT INTO role_permissions VALUES ('admin','knowledge:review')")
reviews.action(job['id'], 'resume', self.actor, '')
KnowledgeReviewWorker(self.store).step()
self.assertEqual(reviews.get(job['id'], self.actor)['approved'], 5)
def test_expiry_and_replaced_previews(self):
self.seed(2)
reviews = KnowledgeReviewStore(self.store); old = reviews.preview('*', -1, self.actor, '')
job = reviews.preview('*', -1, self.actor, '')
with self.assertRaises(ValueError):
reviews.action(old['id'], 'start', self.actor, '', True)
with self.db.connect() as db:
db.execute('UPDATE knowledge_review_job SET expires_at=0 WHERE id=?', (job['id'],))
with self.assertRaises(ValueError):
reviews.action(job['id'], 'start', self.actor, '', True)
while KnowledgeReviewWorker(self.store).step():
pass
with self.assertRaises(KeyError):
reviews.get(job['id'], self.actor)
self.assertEqual(self.store.stats('default'), {'draft': 2})
def test_disabled_reviewer_cannot_start_or_continue(self):
self.seed(3)
reviews = KnowledgeReviewStore(self.store); job = reviews.preview('*', -1, self.actor, '')
with self.db.connect() as db:
db.execute('UPDATE users SET active=0 WHERE id=?', (self.actor,))
with self.assertRaises(ValueError):
reviews.action(job['id'], 'start', self.actor, '', True)
with self.db.connect() as db:
db.execute('UPDATE users SET active=1 WHERE id=?', (self.actor,))
reviews.action(job['id'], 'start', self.actor, '', True)
KnowledgeReviewWorker(self.store).step(1)
with self.db.connect() as db:
db.execute('UPDATE users SET active=0 WHERE id=?', (self.actor,))
KnowledgeReviewWorker(self.store).step()
final = reviews.get(job['id'], self.actor)
self.assertEqual((final['status'], final['approved']), ('failed', 1))
self.assertEqual(self.store.stats('default'), {'approved': 1, 'draft': 2})
class FullReviewApiTest(TestCase):
payload = FullReviewTest.payload
start_job = FullReviewTest.start_job
process = FullReviewTest.process
first = FullReviewTest.first
seed = FullReviewTest.seed
setUp = fixtures.KnowledgeApiTest.setUp
tearDown = fixtures.KnowledgeApiTest.tearDown
def test_auth_confirmation_owner_and_read_only_pagination(self):
url = '/api/v2/knowledge/review-tasks'
self.assertEqual(self.client.post(url + '/preview').status_code, 401)
response = self.client.post(url + '/preview?account_id=-1', headers=self.headers)
self.assertEqual(response.status_code, 200, response.text)
job = response.json()
self.assertEqual(job['total'], 0)
self.assertEqual(self.client.post(url + '/' + job['id'] + '/start', headers=self.headers,
json={'confirmed': False}).status_code, 400)
self.assertEqual(self.client.get(url + '/' + job['id'] + '/results?page_size=1000', headers=self.headers).status_code, 422)
actor = self.db.authenticate('admin', 'ChangedAdmin@2026')['id']
self.db.create_user('reviewer', 'InitialReader123', 'viewer', actor, '')
user_id = self.db.authenticate('reviewer', 'InitialReader123')['id']
self.db.change_password(user_id, 'InitialReader123', 'ChangedReader@2026', '')
login = self.client.post('/api/v2/auth/login', json={'username': 'reviewer', 'password': 'ChangedReader@2026'})
headers = {'Authorization': 'Bearer ' + login.json()['access_token']}
self.assertEqual(self.client.post(url + '/preview', headers=headers).status_code, 403)
with self.db.connect() as db:
db.executemany("INSERT OR IGNORE INTO role_permissions VALUES ('viewer',?)", [(p,) for p in ('knowledge:read','knowledge:review','im:content:read')])
self.assertEqual(self.client.get(url + '/' + job['id'], headers=headers).status_code, 404)
self.assertEqual(self.client.post(url + '/' + job['id'] + '/cancel', headers=headers, json={}).status_code, 404)
def test_restricted_all_accounts_and_revocation_during_background_execution(self):
self.actor = self.db.authenticate('admin', 'ChangedAdmin@2026')['id']
self.store = self.client.app.state.knowledge_store
self.archive = ArchiveStore(self.db)
accounts = []
for index in range(3):
_, account = self.db.create_desktop_session(
{'user_id': f'full-review-{index}', 'sn': f'REV{index}', 'nickname': f'测试账号{index}'},
f'review-device-{index}', 'Review QA', '1.0', '')
accounts.append(account)
self.seed(10 + index, account['tenant_id'])
self.db.create_user('limited-reviewer', 'InitialReader123', 'viewer', self.actor, '')
reviewer = self.db.authenticate('limited-reviewer', 'InitialReader123')['id']
self.db.change_password(reviewer, 'InitialReader123', 'ChangedReader@2026', '')
with self.db.connect() as db:
db.executemany("INSERT OR IGNORE INTO role_permissions VALUES ('viewer',?)",
[(p,) for p in ('knowledge:read','knowledge:review','im:content:read')])
for account in accounts[:2]:
self.db.set_desktop_account_admins(account['id'], [reviewer], self.actor, '')
login = self.client.post('/api/v2/auth/login', json={'username': 'limited-reviewer', 'password': 'ChangedReader@2026'})
headers = {'Authorization': 'Bearer ' + login.json()['access_token']}
url = '/api/v2/knowledge/review-tasks'
self.assertEqual(self.client.post(url + '/preview?account_id=' + str(accounts[2]['id']), headers=headers).status_code, 403)
single = self.client.post(url + '/preview?account_id=' + str(accounts[0]['id']), headers=headers)
self.assertEqual(single.json()['total'], 10)
snapshot = self.client.post(url + '/preview?account_id=-1', headers=headers)
self.assertEqual(snapshot.status_code, 200, snapshot.text)
job = snapshot.json(); self.assertEqual(job['total'], 21)
self.assertEqual(len(job['preview']['accounts']), 2)
self.db.set_desktop_account_admins(accounts[1]['id'], [], self.actor, '')
response = self.client.post(url + '/' + job['id'] + '/start', headers=headers, json={'confirmed': True})
self.assertEqual(response.status_code, 200, response.text)
KnowledgeReviewWorker(self.store).step()
final = self.client.get(url + '/' + job['id'], headers=headers).json()
self.assertEqual((final['approved'], final['skipped']), (10, 11))
self.assertEqual(self.store.stats(accounts[2]['tenant_id']), {'draft': 12})
self.assertEqual(self.store.stats(accounts[1]['tenant_id']), {'draft': 11})
results = self.client.get(url + '/' + job['id'] + '/results', headers=headers).json()
self.assertEqual(results['total'], 0, 'Do not expose titles from revoked accounts')