"""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')