import fcntl import tempfile import unittest from pathlib import Path from page_state import find_marker from publish_one import ( AmbiguousWrite, DeterministicFailure, PublicVerificationError, Publisher, ) WORKSPACE = "workspace" PENDING_OLD = "anwarpy: status=pending | access=free | category=نماذج | queued_at=2026-10-08T09:00:00+03:00" PENDING_NEW = "anwarpy: status=pending | access=vip | category=أدوات | queued_at=2026-10-09T09:00:00+03:00" BLOCKED_OLDER = "anwarpy: status=blocked | access=free | category=نماذج | queued_at=2026-10-07T09:00:00+03:00 | blocked_reason=cover" REPUBLISH = "anwarpy: status=republish | access=vip | category=أدوات | queued_at=2026-10-10T09:00:00+03:00 | slug=existing-guide | published_at=2026-10-09T12:00:00+03:00 | url=https://anwarpy.ai/posts/existing-guide" def blocks(marker, evidence="Public source text"): return [ {"id": "body", "type": "paragraph", "text": evidence, "depth": 0}, {"id": "marker", "type": "paragraph", "text": marker, "depth": 0}, ] class FakeAppFlowy: def __init__(self, page_markers): self.pages = [ {"view_id": page_id, "name": title} for page_id, (title, _marker) in page_markers.items() ] self.block_map = { page_id: blocks(marker) for page_id, (_title, marker) in page_markers.items() } self.list_calls = 0 self.edits = [] def list_pages(self): self.list_calls += 1 return self.pages def get_blocks(self, page_id): return [dict(block) for block in self.block_map[page_id]] def edit_block(self, page_id, block_id, text): self.edits.append((page_id, block_id, text)) for block in self.block_map[page_id]: if block["id"] == block_id: block["text"] = text return raise KeyError(block_id) def marker(self, page_id): return find_marker(self.get_blocks(page_id)).meta class FakeGenerator: def __init__(self, slugs=None): self.slugs = slugs or {} self.calls = [] def generate(self, title, evidence, meta): self.calls.append((title, evidence, dict(meta))) slug = self.slugs.get(title, "generated-guide") return { "title": f"دليل {title}", "slug": slug, "dek": "شرح عربي عملي ومباشر", "teaser": "مقدمة مجانية مفيدة" if meta["access"] == "vip" else "", "sections": [ {"heading": "المشكلة", "paragraphs": ["شرح واضح للمشكلة والحل."], "code": None}, {"heading": "التحقق", "paragraphs": ["تحقق من النتيجة بخطوة قابلة للقياس."], "code": None}, {"heading": "القيود", "paragraphs": ["راجع القيود قبل الاستخدام."], "code": None}, ], } class FakeMedia: def __init__(self): self.calls = [] def render(self, article, meta): self.calls.append((article["slug"], dict(meta))) return { "coverImage": {"filename": f"{article['slug']}.jpg", "dataBase64": "/9j/2Q=="}, "video_path": f"/tmp/{article['slug']}.mp4", } class FakeApi: def __init__(self, records=None, timeout_after_create=False, public_ok=True): self.records = records or {} self.timeout_after_create = timeout_after_create self.public_ok = public_ok self.create_calls = [] self.update_calls = [] self.verify_calls = [] def lookup(self, slug): record = self.records.get(slug) return dict(record) if record else None def create(self, body): self.create_calls.append(dict(body)) record = { "id": len(self.records) + 1, "title": body["title"], "slug": body["slug"], "status": "published" if body["publish"] else "draft", "access": body["access"], "category": body["category"], "publishedAt": "2026-10-09T12:20:11+00:00", "url": f"https://anwarpy.ai/posts/{body['slug']}", } self.records[body["slug"]] = record if self.timeout_after_create: self.timeout_after_create = False raise AmbiguousWrite("timeout after create") return dict(record) def update(self, slug, body): self.update_calls.append((slug, dict(body))) old = self.records[slug] old.update({ "title": body.get("title", old.get("title")), "status": "published" if body.get("publish") else old["status"], "access": body.get("access", old["access"]), "category": body.get("category", old["category"]), "publishedAt": old.get("publishedAt") or "2026-10-09T12:20:11+00:00", "url": f"https://anwarpy.ai/posts/{slug}", }) return dict(old) def verify_public(self, record): self.verify_calls.append(dict(record)) return self.public_ok class PublisherTests(unittest.TestCase): def make_publisher(self, app, api, generator=None, media=None, lock_path=None): return Publisher( appflowy=app, api=api, generator=generator or FakeGenerator(), media=media or FakeMedia(), lock_path=lock_path or Path(tempfile.mkdtemp()) / "publisher.lock", ) def test_lock_loser_makes_no_external_calls(self): lock_path = Path(tempfile.mkdtemp()) / "publisher.lock" app = FakeAppFlowy({"page": ("Topic", PENDING_OLD)}) api = FakeApi() with lock_path.open("a+") as handle: fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) result = self.make_publisher(app, api, lock_path=lock_path).run() self.assertEqual(result, "busy") self.assertEqual(app.list_calls, 0) def test_fifo_uses_queued_at_and_skips_blocked(self): app = FakeAppFlowy({ "blocked": ("Blocked", BLOCKED_OLDER), "new": ("New", PENDING_NEW), "old": ("Old", PENDING_OLD), }) generator = FakeGenerator({"Old": "old-guide", "New": "new-guide"}) result = self.make_publisher(app, FakeApi(), generator=generator).run() self.assertEqual(result, "published:old-guide") self.assertEqual(generator.calls[0][0], "Old") self.assertEqual(app.marker("blocked")["status"], "blocked") self.assertEqual(app.marker("new")["status"], "pending") def test_pending_absent_slug_posts_once_and_rerun_is_idle(self): app = FakeAppFlowy({"page": ("Topic", PENDING_OLD)}) api = FakeApi() publisher = self.make_publisher(app, api) self.assertEqual(publisher.run(), "published:generated-guide") self.assertEqual(len(api.create_calls), 1) self.assertEqual(app.marker("page")["status"], "published") self.assertEqual(publisher.run(), "idle") self.assertEqual(len(api.create_calls), 1) def test_existing_draft_is_promoted_without_post(self): record = { "id": 1, "title": "Draft", "slug": "generated-guide", "status": "draft", "access": "free", "category": "نماذج", "publishedAt": None, } app = FakeAppFlowy({"page": ("Topic", PENDING_OLD)}) api = FakeApi({"generated-guide": record}) self.assertEqual(self.make_publisher(app, api).run(), "published:generated-guide") self.assertEqual(api.create_calls, []) self.assertEqual(len(api.update_calls), 1) def test_republish_updates_existing_slug_and_never_posts(self): record = { "id": 7, "title": "Old", "slug": "existing-guide", "status": "published", "access": "vip", "category": "أدوات", "publishedAt": "2026-10-09T09:00:00Z", "url": "https://anwarpy.ai/posts/existing-guide", } app = FakeAppFlowy({"page": ("Topic", REPUBLISH)}) api = FakeApi({"existing-guide": record}) generator = FakeGenerator({"Topic": "existing-guide"}) self.assertEqual(self.make_publisher(app, api, generator=generator).run(), "published:existing-guide") self.assertEqual(api.create_calls, []) self.assertEqual(len(api.update_calls), 1) def test_ambiguous_create_reconciles_without_second_post(self): app = FakeAppFlowy({"page": ("Topic", PENDING_OLD)}) api = FakeApi(timeout_after_create=True) self.assertEqual(self.make_publisher(app, api).run(), "published:generated-guide") self.assertEqual(len(api.create_calls), 1) def test_public_failure_leaves_publishing_and_does_not_repost(self): app = FakeAppFlowy({"page": ("Topic", PENDING_OLD)}) api = FakeApi(public_ok=False) publisher = self.make_publisher(app, api) with self.assertRaises(PublicVerificationError): publisher.run() self.assertEqual(app.marker("page")["status"], "publishing") self.assertEqual(len(api.create_calls), 1) with self.assertRaises(PublicVerificationError): publisher.run() self.assertEqual(len(api.create_calls), 1) def test_api_metadata_mismatch_leaves_publishing(self): app = FakeAppFlowy({"page": ("Topic", PENDING_OLD)}) api = FakeApi() original_create = api.create def mismatched(body): result = original_create(body) result["access"] = "vip" api.records[body["slug"]]["access"] = "vip" return result api.create = mismatched with self.assertRaisesRegex(ValueError, "access mismatch"): self.make_publisher(app, api).run() self.assertEqual(app.marker("page")["status"], "publishing") self.assertEqual(len(api.create_calls), 1) def test_generation_failure_blocks_page_without_api_write(self): app = FakeAppFlowy({"page": ("Topic", PENDING_OLD)}) api = FakeApi() generator = FakeGenerator() def fail(*_args): raise DeterministicFailure("generation", "model failed") generator.generate = fail self.assertEqual(self.make_publisher(app, api, generator=generator).run(), "blocked:generation") self.assertEqual(app.marker("page")["status"], "blocked") self.assertEqual(api.create_calls, []) def test_malformed_marker_is_reported_and_valid_page_can_run(self): app = FakeAppFlowy({ "bad": ("Bad", "Anwarpy: pending"), "good": ("Good", PENDING_OLD), }) publisher = self.make_publisher(app, FakeApi(), generator=FakeGenerator({"Good": "good-guide"})) self.assertEqual(publisher.run(), "published:good-guide") self.assertEqual(len(publisher.marker_errors), 1) self.assertIn("bad", publisher.marker_errors[0]) if __name__ == "__main__": unittest.main()