feat: add deterministic Anwarpy publisher

This commit is contained in:
AHMED
2026-10-09 15:58:13 +03:00
parent 4aeb747d56
commit 54dfe2ff3c
4 changed files with 615 additions and 8 deletions
+269
View File
@@ -0,0 +1,269 @@
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_puts_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()