import hashlib import json from pathlib import Path import shutil import sys import tempfile import unittest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from articles import inspect_article, short_title from store import Store class QueueTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.root = Path(self.tmp.name) self.article = self.root / 'article' self.article.mkdir() self.md = self.article / 'a.md' self.md.write_text('# 测试新闻\n\n正文内容。\n\n来源:测试来源,如有侵权,请联系删除。\n') self.store = Store(self.root / 'queue.sqlite3') self.store.configure(dict(fid=125, minimum=1, maximum=1, interval=0, history_checked=True)) self.owner = dict(owner='tab-a', account=dict(uid='123', name='测试账号'), extension={'idle': True}) def tearDown(self): self.tmp.cleanup() def begin(self): self.store.enqueue(self.article) self.store.control('start') return self.store.next(self.owner)['job'] def transition(self, job, state, **extra): return self.store.event(dict(owner='tab-a', id=job['id'], state=state, epoch=self.store.run['epoch'], **extra)) def submit(self, job): for state in ('importing', 'formatting', 'awaiting_user_review'): self.transition(job, state) self.transition(job, 'user_submitting', snapshot={'title': job['title']}) def accept(self, job): return self.store.accept(dict(owner='tab-a', id=job['id'], post_url='https://www.dww11.com/thread/12345.html', author_uid='123', fid=125, title=job['title'], state='accepted_pending_moderation', evidence={'test': 'fixture only'})) def test_stable_identity_and_original_preserved(self): before = self.md.read_bytes() item = inspect_article(self.article) copy = self.root / 'renamed' shutil.copytree(self.article, copy) (copy / 'a.md').write_text(self.md.read_text().replace('测试新闻', '改标题')) self.assertEqual(inspect_article(copy)['article_key'], item['article_key']) self.assertEqual(self.md.read_bytes(), before) self.assertEqual(item['source'], '测试来源') def test_title_does_not_break_emoji(self): short = short_title('😀' * 50) self.assertTrue(short.endswith('...')) self.assertLessEqual(len(short.encode('utf-16-le')) // 2, 80) self.assertEqual(short_title('短标题'), '短标题') def test_unsupported_images_stop_before_queue(self): (self.article / 'a.webp').write_bytes(b'webp') with self.assertRaisesRegex(ValueError, '暂不支持'): self.store.enqueue(self.article) self.assertEqual(self.store.state()['jobs'], []) def test_missing_and_outside_image_rejected(self): for image in ('missing.png', '../outside.png', 'https://example.com/a.png'): self.md.write_text('# 标题\n\n正文 ![](' + image + ')\n\n来源:测试,如有侵权,请联系删除。') with self.assertRaises(ValueError): self.store.enqueue(self.article) def test_no_duplicate_queue_or_cross_tab_claim(self): job = self.begin() self.assertIn('已在队列', self.store.enqueue(self.article)) self.assertEqual(self.store.next({**self.owner, 'owner': 'tab-b'})['action'], 'other_tab') self.assertEqual(self.store.next(self.owner)['job']['id'], job['id']) def test_stop_invalidates_old_continuation(self): job = self.begin() epoch = self.store.run['epoch'] self.store.control('stop') self.store.control('start') with self.assertRaisesRegex(ValueError, '过期'): self.store.event(dict(owner='tab-a', id=job['id'], state='importing', epoch=epoch)) def test_restart_pauses_and_preserves_claim(self): job = self.begin() restarted = Store(self.root / 'queue.sqlite3') self.assertFalse(restarted.run['running']) self.assertEqual(restarted.run['active'], job['id']) self.assertEqual(restarted.run['batch']['target'], 1) def test_accept_requires_manual_submission(self): job = self.begin() with self.assertRaisesRegex(ValueError, '人工提交'): self.accept(job) def test_accept_is_atomic_and_idempotent(self): job = self.begin() self.submit(job) self.accept(job) self.accept(job) self.assertEqual(self.store.run['batch']['count'], 1) self.assertIn('已受理', self.store.enqueue(self.article)) with self.assertRaisesRegex(ValueError, '已受理'): self.store.resolve(dict(id=job['id'], confirmed_not_accepted=True, retry=True)) def test_unknown_never_requeues_automatically(self): job = self.begin() self.submit(job) self.transition(job, 'submit_unknown') self.assertEqual(self.store.next(self.owner)['job']['state'], 'submit_unknown') self.assertEqual(self.store.run['batch']['count'], 0) self.assertIsNone(self.store.run['switch']) def test_migrate_real_ledger_shape(self): record = dict(markdown_sha256=hashlib.sha256(self.md.read_bytes()).hexdigest(), block_repost=True, markdown_path=str(self.md), status='accepted_pending_moderation', post_id='12345') self.store.import_history([record]) self.assertIn('已受理', self.store.enqueue(self.article)) def test_switch_only_after_target_and_with_more_work(self): job = self.begin() self.submit(job) self.accept(job) other = self.root / 'other';shutil.copytree(self.article, other) (other / 'a.md').write_text(self.md.read_text().replace('正文内容', '另一篇内容')) self.store.enqueue(other) self.assertEqual(self.store.next(self.owner)['action'], 'switch') self.assertEqual(self.store.next(self.owner)['action'], 'switch') self.store.switch_begin({'owner': 'tab-a'}) with self.assertRaisesRegex(ValueError, '禁止重复'): self.store.switch_begin({'owner': 'tab-a'}) self.assertEqual(self.store.next(self.owner)['action'], 'switch_wait') next_job = self.store.next({**self.owner, 'account': {'uid': '456', 'name': '新测试账号'}}) self.assertEqual(next_job['action'], 'prepare') self.assertEqual(self.store.run['batch']['count'], 0) def test_no_switch_when_queue_done(self): job = self.begin();self.submit(job);self.accept(job) self.assertEqual(self.store.next(self.owner)['action'], 'idle') self.assertIsNone(self.store.run['switch']) def test_bad_result_does_not_increment(self): job = self.begin();self.submit(job) with self.assertRaises(ValueError): self.store.accept(dict(owner='tab-a', id=job['id'], post_url='https://example.com/thread/12345.html')) self.assertEqual(self.store.run['batch']['count'], 0) if __name__ == '__main__': unittest.main()