facebook: the first post drafted (COPY.md section 6) + fb_probe schedule-post
gates / gates (push) Successful in 5m58s
gates / gates (push) Successful in 5m58s
DRAFTS. COPY.md section 6: two versions of the Page's first post, shortened
from section 2. Hungarian, tegezo, no price. 6.1 = 347 characters, 6.2 = 638
(counted as Unicode characters). Every claim carries a source comment naming
the line of website/index.html it rests on; the first line of each carries the
point alone, because Facebook cuts after about three lines.
Deliberate: ZERO emoji and ZERO hashtags, though the brief allows two of each.
The Felhom design system uses no emoji (the website gate holds it at 0) and
two hashtags would serve no real search.
CHECKED, because the post repeats it: "56 alkalmazas" is CORRECT. The apps
page carries 57 <div class="app-card"> but states 56, which looks off by one
until you read the category line -- "6 alkalmazas + 1 beepitett". The 57th
card is FileBrowser, built into every box and deliberately not counted.
index.html says "56 telepitheto alkalmazas" too. I nearly "fixed" a live page
into being wrong. NOT-A-FINDING.
SCHEDULE-POST. A third sub-command on fb_probe.py, reusing its token loader
(R-453), redaction, Bearer call and evidence writer:
- the body is READ FROM COPY.md by section name. The Hungarian never passes
through a shell or an argv string (brief 9.6); the caller names a section.
- published is ALWAYS "false" and NO argument can change it (brief 9.7).
The operator's review in Planner is the safety net, so an immediate post
must be unreachable, not merely not-the-default.
- check_when refuses a time under Meta's 10-minute floor or over its
6-month ceiling, BEFORE the call, so a bad time is a readable local
refusal rather than a Graph error.
- budapest_to_epoch uses the real tz database. If zoneinfo has no
Europe/Budapest it REFUSES rather than falling back to a hardcoded
+01:00/+02:00 -- guessing the offset is how a post goes out an hour wrong
across a DST boundary.
- list_scheduled tries /scheduled_posts then feed?is_published=false and
RECORDS WHICH ANSWERED; when both are refused it returns None so the
caller says "unproven" instead of claiming a removal it never saw.
TESTS: 30, of which 2 skip on Windows (no tzdata in this interpreter; the
command runs on the Linux host, which has the system zoneinfo).
RED-PROOF of the guard that matters, as the brief requires. Made
schedule_form accept published=..., ran ScheduleForm, and watched
test_no_argument_can_publish_immediately FAIL:
AssertionError: 'true' != 'false' : published changed published
Guard restored, all 30 green again.
No post has been made. The dry check and the real post come next; the
operator picks the version and the time first.
This commit is contained in:
@@ -81,5 +81,111 @@ class GoneError(unittest.TestCase):
|
||||
self.assertFalse(fb_probe.gone_error(None))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- schedule-post (R-917)
|
||||
|
||||
class ScheduleForm(unittest.TestCase):
|
||||
"""The guard that matters: this command can only SCHEDULE. The operator's review in Planner is
|
||||
the safety net, so an immediate post must be unreachable — not merely undefaulted."""
|
||||
|
||||
def test_published_is_always_false(self):
|
||||
f = fb_probe.schedule_form("x", fb_probe.time.time() + 3600)
|
||||
self.assertEqual(f["published"], "false")
|
||||
|
||||
def test_no_argument_can_publish_immediately(self):
|
||||
# every keyword the function accepts, tried: none of them flips `published`
|
||||
import inspect
|
||||
names = [p for p in inspect.signature(fb_probe.schedule_form).parameters if p not in ("message", "when")]
|
||||
for name in names:
|
||||
f = fb_probe.schedule_form("x", fb_probe.time.time() + 3600, **{name: "true"})
|
||||
self.assertEqual(f["published"], "false", "%s changed published" % name)
|
||||
|
||||
def test_link_is_included_by_default(self):
|
||||
f = fb_probe.schedule_form("x", fb_probe.time.time() + 3600)
|
||||
self.assertEqual(f["link"], "https://felhom.eu/")
|
||||
|
||||
def test_link_can_be_dropped(self):
|
||||
f = fb_probe.schedule_form("x", fb_probe.time.time() + 3600, link=None)
|
||||
self.assertNotIn("link", f)
|
||||
|
||||
|
||||
class ScheduleWindow(unittest.TestCase):
|
||||
NOW = 1_760_000_000
|
||||
|
||||
def test_too_soon_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
fb_probe.check_when(self.NOW + 9 * 60, now=self.NOW)
|
||||
|
||||
def test_in_the_past_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
fb_probe.check_when(self.NOW - 60, now=self.NOW)
|
||||
|
||||
def test_too_far_is_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
fb_probe.check_when(self.NOW + fb_probe.SCHED_MAX_S + 60, now=self.NOW)
|
||||
|
||||
def test_ten_minutes_exactly_is_allowed(self):
|
||||
self.assertEqual(fb_probe.check_when(self.NOW + 600, now=self.NOW), self.NOW + 600)
|
||||
|
||||
def test_a_normal_time_is_allowed(self):
|
||||
self.assertEqual(fb_probe.check_when(self.NOW + 86400, now=self.NOW), self.NOW + 86400)
|
||||
|
||||
def test_schedule_form_refuses_a_bad_time_too(self):
|
||||
# the guard is not only on check_when: the form builder must not assemble a bad post
|
||||
with self.assertRaises(ValueError):
|
||||
fb_probe.schedule_form("x", 0)
|
||||
|
||||
|
||||
class CopySection(unittest.TestCase):
|
||||
"""The post body is READ FROM COPY.md, never passed through a shell (brief 9.6)."""
|
||||
|
||||
def test_reads_the_fenced_block(self):
|
||||
txt = fb_probe.read_copy_section(fb_probe.COPY_MD, "6.1 Rövid változat")
|
||||
self.assertIn("felhom.eu/kapcsolat", txt)
|
||||
self.assertFalse(txt.startswith("```"))
|
||||
self.assertFalse(txt.endswith(chr(10)))
|
||||
|
||||
def test_sections_differ(self):
|
||||
a = fb_probe.read_copy_section(fb_probe.COPY_MD, "6.1 Rövid változat")
|
||||
b = fb_probe.read_copy_section(fb_probe.COPY_MD, "6.2 Közepes változat")
|
||||
self.assertNotEqual(a, b)
|
||||
self.assertGreater(len(b), len(a))
|
||||
|
||||
def test_missing_section_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
fb_probe.read_copy_section(fb_probe.COPY_MD, "9.9 nincs ilyen")
|
||||
|
||||
|
||||
class ScheduleRedaction(unittest.TestCase):
|
||||
def test_a_token_in_a_scheduled_form_is_redacted(self):
|
||||
f = fb_probe.schedule_form("see " + FAKE, fb_probe.time.time() + 3600)
|
||||
self.assertNotIn(FAKE, fb_probe.redact(f, secrets=[FAKE])["message"])
|
||||
|
||||
|
||||
def _has_budapest_tz():
|
||||
try:
|
||||
from zoneinfo import ZoneInfo
|
||||
ZoneInfo("Europe/Budapest")
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@unittest.skipUnless(_has_budapest_tz(),
|
||||
"no Europe/Budapest in this interpreter's tz database (Windows ships none; "
|
||||
"the command runs on the Linux host, which has the system zoneinfo)")
|
||||
class BudapestTime(unittest.TestCase):
|
||||
def test_summer_and_winter_offsets_differ(self):
|
||||
# CEST in July (+02:00), CET in January (+01:00) — the tz database, not a guessed offset
|
||||
jul = fb_probe.budapest_to_epoch("2026-07-01 19:00")
|
||||
jan = fb_probe.budapest_to_epoch("2026-01-01 19:00")
|
||||
import datetime
|
||||
self.assertEqual(datetime.datetime.utcfromtimestamp(jul).hour, 17)
|
||||
self.assertEqual(datetime.datetime.utcfromtimestamp(jan).hour, 18)
|
||||
|
||||
def test_bad_format_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
fb_probe.budapest_to_epoch("tomorrow evening")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user