mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 19:37:03 +01:00
release: 0.1.6 «Zephyrinus» — 21:37 reports today, not yesterday
The scheduler fired exactly on time tonight and posted nothing, because publish_report covered the previous full day and the bot's first recorded day was today: "no messages in group.WTI5dVptbG... on 2026-08-29". Correct by code, a day late to everybody reading the group. So the report covers the day it runs on. The count closes at the report time and render_day says so, because a total that quietly omits three hours is worse than one that admits to it. Everything that made a late day recoverable stays: the per-group marker still prevents a double post, and days_to_report still yields every day an outage swallowed. closing_periods keeps taking a day rather than reading a clock, so a week now closes on the Sunday evening it ends -- off a day that is itself still running -- and a catch-up on a week-old Sunday still gets it right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
This commit is contained in:
parent
1bf63b053b
commit
50bd390b00
@ -71,6 +71,15 @@ this repo: describe what changes for them. 0.0.1 shipped a bullet about
|
|||||||
`/healthz`, an endpoint bound to 127.0.0.1 that nobody in a Signal group can
|
`/healthz`, an endpoint bound to 127.0.0.1 that nobody in a Signal group can
|
||||||
reach, and it was rightly called out.
|
reach, and it was rightly called out.
|
||||||
|
|
||||||
|
## 0.1.6 — Zephyrinus
|
||||||
|
|
||||||
|
- statystyki o 21:37 są od teraz o **dzisiejszym** dniu, a nie o wczorajszym —
|
||||||
|
wieczorny post pokazuje wreszcie ten dzień, który właśnie mijał
|
||||||
|
- licznik zamyka się o 21:37, więc to, co napiszecie później, wpada do jutra;
|
||||||
|
post sam to mówi, żeby liczba nie udawała całej doby
|
||||||
|
- dzień, którego bot przegapił (bo go nie było o 21:37), nadal zostanie
|
||||||
|
wysłany później — nic nie ginie
|
||||||
|
|
||||||
## 0.1.5 — Victor I
|
## 0.1.5 — Victor I
|
||||||
|
|
||||||
- ogłoszenie nowej wersji przychodzi teraz ze zdjęciem papieża, po którym ta
|
- ogłoszenie nowej wersji przychodzi teraz ze zdjęciem papieża, po którym ta
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "signal-bot"
|
name = "signal-bot"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
description = "A Signal group bot: image commands and daily message tallies."
|
description = "A Signal group bot: image commands and daily message tallies."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@ -82,18 +82,24 @@ class Bot:
|
|||||||
clock: Clock = now_local
|
clock: Clock = now_local
|
||||||
|
|
||||||
async def publish_report(self) -> None:
|
async def publish_report(self) -> None:
|
||||||
"""Post each group's report for the last full day.
|
"""Post each group's report for today, plus any day still owed.
|
||||||
|
|
||||||
Yesterday, not "since the last post". The old scheme reset a counter
|
Today, because 21:37 is when the group expects to read its own
|
||||||
after publishing, so a group nobody spoke in was never reset and its
|
numbers; a post at 21:37 about yesterday is a day late to everyone
|
||||||
next report silently covered two days. A day is now a property of
|
looking at it. The count therefore closes at the report time and the
|
||||||
when a message was sent, which cannot drift.
|
rest of the evening lands in tomorrow's day -- render_day says so,
|
||||||
|
because a total that quietly omits three hours is worse than one that
|
||||||
|
admits to it.
|
||||||
|
|
||||||
|
Everything that makes a late day recoverable stays: the day is marked
|
||||||
|
per group, so it cannot post twice, and days_to_report still yields
|
||||||
|
every day the bot was down for, oldest first.
|
||||||
|
|
||||||
Nothing is deleted afterwards. Each group is isolated on purpose: one
|
Nothing is deleted afterwards. Each group is isolated on purpose: one
|
||||||
group refusing the report -- the bot removed from it, a transient send
|
group refusing the report -- the bot removed from it, a transient send
|
||||||
failure -- must not cost every other group its report.
|
failure -- must not cost every other group its report.
|
||||||
"""
|
"""
|
||||||
latest = periods.previous_day(self.clock())
|
latest = periods.today(self.clock())
|
||||||
for group_id in self.directory.groups:
|
for group_id in self.directory.groups:
|
||||||
for day in days_to_report(self.store, group_id, latest):
|
for day in days_to_report(self.store, group_id, latest):
|
||||||
if not await self._report_one(group_id, day):
|
if not await self._report_one(group_id, day):
|
||||||
@ -125,6 +131,10 @@ class Bot:
|
|||||||
since=self.store.events.first_day(group_id),
|
since=self.store.events.first_day(group_id),
|
||||||
approximate=backfilled == day,
|
approximate=backfilled == day,
|
||||||
highlights=tuple(pick.text for pick in picks),
|
highlights=tuple(pick.text for pick in picks),
|
||||||
|
# Only for a day that is still running. A day caught up after
|
||||||
|
# an outage is complete, and telling the group its count
|
||||||
|
# stopped at 21:37 would be a lie about a finished day.
|
||||||
|
closed_at=self._closing_time(day),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@ -134,6 +144,12 @@ class Bot:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _closing_time(self, day: str) -> str | None:
|
||||||
|
"""The "HH:MM" a still-open day was counted up to, or None."""
|
||||||
|
if day != periods.today(self.clock()):
|
||||||
|
return None
|
||||||
|
return f"{self.config.report_hour:02d}:{self.config.report_minute:02d}"
|
||||||
|
|
||||||
async def _report_closing(self, group_id: str, day: str) -> None:
|
async def _report_closing(self, group_id: str, day: str) -> None:
|
||||||
"""Post the week, month or year that ended with this day, if any.
|
"""Post the week, month or year that ended with this day, if any.
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,7 @@ def days_to_report(store: Store, group_id: str, latest: str) -> list[str]:
|
|||||||
Args:
|
Args:
|
||||||
store: The open database, which holds the per-group marker.
|
store: The open database, which holds the per-group marker.
|
||||||
group_id: The group being reported on.
|
group_id: The group being reported on.
|
||||||
latest: The most recent day eligible for a report, normally yesterday.
|
latest: The most recent day eligible for a report, normally today.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The days to post, oldest first. A group with no marker gets only
|
The days to post, oldest first. A group with no marker gets only
|
||||||
@ -69,10 +69,11 @@ LAST_DAY_OF_DECEMBER: Final = "12-31"
|
|||||||
def closing_periods(day: str) -> list[Period]:
|
def closing_periods(day: str) -> list[Period]:
|
||||||
"""The longer periods that ended with ``day``, in the order to post them.
|
"""The longer periods that ended with ``day``, in the order to post them.
|
||||||
|
|
||||||
Keyed off the day being *reported*, not off today. The nightly post covers
|
Keyed off the day being *reported*, never off the calendar. The nightly
|
||||||
yesterday, so a week is complete exactly when yesterday was a Sunday --
|
post covers the day it runs on, so a week is complete exactly when that
|
||||||
which is also the only reading that stays correct when the bot has been
|
day is a Sunday -- and the same reading stays correct when the bot has
|
||||||
offline and is catching up on a week-old day.
|
been offline and is catching up on a week-old day, which is why the
|
||||||
|
predicate takes the day rather than reading a clock.
|
||||||
|
|
||||||
No separate scheduler for these. Three background tasks firing at the same
|
No separate scheduler for these. Three background tasks firing at the same
|
||||||
wall-clock time into the same group is three independent ways for the
|
wall-clock time into the same group is three independent ways for the
|
||||||
|
|||||||
@ -87,6 +87,10 @@ class DayView:
|
|||||||
heading: str | None = None
|
heading: str | None = None
|
||||||
# The day's messages have counts but no lengths -- see stats.backfill.
|
# The day's messages have counts but no lengths -- see stats.backfill.
|
||||||
approximate: bool = False
|
approximate: bool = False
|
||||||
|
# "21:37" when the day is still running and the count stopped there, so
|
||||||
|
# the block never presents a partial day as a whole one. None for a
|
||||||
|
# finished day, which needs no caveat.
|
||||||
|
closed_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
def render_day(
|
def render_day(
|
||||||
@ -110,6 +114,8 @@ def render_day(
|
|||||||
if line
|
if line
|
||||||
]
|
]
|
||||||
picks = list(view.highlights)
|
picks = list(view.highlights)
|
||||||
|
if view.closed_at:
|
||||||
|
tail.append(f"(licznik zamknięty o {view.closed_at}; reszta wieczoru wpada do jutra)")
|
||||||
if view.since:
|
if view.since:
|
||||||
tail.append(f"(liczymy od {pl_day_month(view.since)})")
|
tail.append(f"(liczymy od {pl_day_month(view.since)})")
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ PATCHES_PER_MINOR: Final = 8
|
|||||||
|
|
||||||
# Which release this build is. Bump by one per release; everything else,
|
# Which release this build is. Bump by one per release; everything else,
|
||||||
# including the version string in pyproject.toml, follows from it.
|
# including the version string in pyproject.toml, follows from it.
|
||||||
RELEASE_ORDINAL: Final = 14
|
RELEASE_ORDINAL: Final = 15
|
||||||
|
|
||||||
_POPES_FILE: Final = Path(__file__).with_name("popes.json")
|
_POPES_FILE: Final = Path(__file__).with_name("popes.json")
|
||||||
|
|
||||||
|
|||||||
@ -8,13 +8,13 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from signal_bot.bot import build_bot
|
from signal_bot.bot import Bot, build_bot
|
||||||
from signal_bot.liveness import Liveness
|
from signal_bot.liveness import Liveness
|
||||||
from signal_bot.stats.nightly import LAST_REPORTED_KEY, mark_reported
|
from signal_bot.stats.nightly import LAST_REPORTED_KEY, mark_reported
|
||||||
from signal_bot.stats.periods import WARSAW
|
from signal_bot.stats.periods import WARSAW
|
||||||
from signal_bot.transport_fake import FakeTransport
|
from signal_bot.transport_fake import FakeTransport
|
||||||
from tests.conftest import WATCHED_GROUP, mock_client
|
from tests.conftest import WATCHED_GROUP, mock_client
|
||||||
from tests.test_bot_report import YESTERDAY, RefusingTransport, _bot, wrote
|
from tests.test_bot_report import TODAY, YESTERDAY, RefusingTransport, _bot, wrote
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from signal_bot.config import Config
|
from signal_bot.config import Config
|
||||||
@ -22,6 +22,21 @@ if TYPE_CHECKING:
|
|||||||
from signal_bot.store import Store
|
from signal_bot.store import Store
|
||||||
|
|
||||||
|
|
||||||
|
def _at(
|
||||||
|
config: Config,
|
||||||
|
store: Store,
|
||||||
|
transport: FakeTransport,
|
||||||
|
directory: GroupDirectory,
|
||||||
|
day: str,
|
||||||
|
) -> Bot:
|
||||||
|
"""A bot whose clock reads 21:37 on ``day``, which is the day it reports."""
|
||||||
|
at = dt.datetime.fromisoformat(day).replace(hour=21, minute=37, tzinfo=WARSAW)
|
||||||
|
client = mock_client(lambda r: httpx.Response(200))
|
||||||
|
return build_bot(
|
||||||
|
config, client, store, transport, directory, Liveness(config, client), clock=lambda: at
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_a_missed_day_is_posted_when_the_bot_returns(
|
async def test_a_missed_day_is_posted_when_the_bot_returns(
|
||||||
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -42,7 +57,7 @@ async def test_a_missed_day_is_posted_when_the_bot_returns(
|
|||||||
assert "31 sierpnia" in transport.texts[2]
|
assert "31 sierpnia" in transport.texts[2]
|
||||||
assert "sierpień 2026" in transport.texts[3]
|
assert "sierpień 2026" in transport.texts[3]
|
||||||
assert "1 września" in transport.texts[4]
|
assert "1 września" in transport.texts[4]
|
||||||
assert store.meta(WATCHED_GROUP, LAST_REPORTED_KEY) == YESTERDAY
|
assert store.meta(WATCHED_GROUP, LAST_REPORTED_KEY) == TODAY
|
||||||
|
|
||||||
|
|
||||||
async def test_a_day_nobody_spoke_is_settled_not_retried_forever(
|
async def test_a_day_nobody_spoke_is_settled_not_retried_forever(
|
||||||
@ -54,7 +69,7 @@ async def test_a_day_nobody_spoke_is_settled_not_retried_forever(
|
|||||||
await _bot(config, store, transport, directory).publish_report()
|
await _bot(config, store, transport, directory).publish_report()
|
||||||
|
|
||||||
assert len(transport.texts) == 1
|
assert len(transport.texts) == 1
|
||||||
assert store.meta(WATCHED_GROUP, LAST_REPORTED_KEY) == YESTERDAY
|
assert store.meta(WATCHED_GROUP, LAST_REPORTED_KEY) == TODAY
|
||||||
|
|
||||||
|
|
||||||
async def test_a_failed_send_leaves_its_day_unmarked(
|
async def test_a_failed_send_leaves_its_day_unmarked(
|
||||||
@ -84,30 +99,39 @@ async def test_publishing_twice_posts_once(
|
|||||||
async def test_a_sunday_also_posts_the_week(
|
async def test_a_sunday_also_posts_the_week(
|
||||||
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
||||||
) -> None:
|
) -> None:
|
||||||
"""One scheduler, a date predicate -- not three tasks racing at 21:37."""
|
"""One scheduler, a date predicate -- not three tasks racing at 21:37.
|
||||||
|
|
||||||
|
The week now closes on the Sunday evening it ends, off a day that is
|
||||||
|
itself still running. That is the point of the predicate taking a day
|
||||||
|
instead of reading a clock: partial or not, Sunday is the last day of
|
||||||
|
the week and the summary belongs to it.
|
||||||
|
"""
|
||||||
sunday = "2026-08-30"
|
sunday = "2026-08-30"
|
||||||
wrote(store, WATCHED_GROUP, "u1", "Alice", 4, day="2026-08-28")
|
wrote(store, WATCHED_GROUP, "u1", "Alice", 4, day="2026-08-28")
|
||||||
wrote(store, WATCHED_GROUP, "u1", "Alice", 6, day=sunday)
|
wrote(store, WATCHED_GROUP, "u1", "Alice", 6, day=sunday)
|
||||||
mark_reported(store, WATCHED_GROUP, "2026-08-29")
|
mark_reported(store, WATCHED_GROUP, "2026-08-29")
|
||||||
at_monday = dt.datetime(2026, 8, 31, 21, 37, tzinfo=WARSAW)
|
|
||||||
|
|
||||||
client = mock_client(lambda r: httpx.Response(200))
|
await _at(config, store, transport, directory, sunday).publish_report()
|
||||||
bot = build_bot(
|
|
||||||
config,
|
|
||||||
client,
|
|
||||||
store,
|
|
||||||
transport,
|
|
||||||
directory,
|
|
||||||
Liveness(config, client),
|
|
||||||
clock=lambda: at_monday,
|
|
||||||
)
|
|
||||||
await bot.publish_report()
|
|
||||||
|
|
||||||
assert len(transport.texts) == 2
|
assert len(transport.texts) == 2
|
||||||
assert "Niedziela, 30 sierpnia" in transport.texts[0]
|
assert "Niedziela, 30 sierpnia" in transport.texts[0]
|
||||||
assert "tydzień 35" in transport.texts[1]
|
assert "tydzień 35" in transport.texts[1]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_a_saturday_closes_nothing(
|
||||||
|
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
||||||
|
) -> None:
|
||||||
|
"""The day before the week ends is still just a day."""
|
||||||
|
saturday = "2026-08-29"
|
||||||
|
wrote(store, WATCHED_GROUP, "u1", "Alice", 4, day=saturday)
|
||||||
|
mark_reported(store, WATCHED_GROUP, "2026-08-28")
|
||||||
|
|
||||||
|
await _at(config, store, transport, directory, saturday).publish_report()
|
||||||
|
|
||||||
|
assert len(transport.texts) == 1
|
||||||
|
assert "Sobota, 29 sierpnia" in transport.texts[0]
|
||||||
|
|
||||||
|
|
||||||
async def test_an_ordinary_day_posts_only_the_day(
|
async def test_an_ordinary_day_posts_only_the_day(
|
||||||
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -134,15 +158,5 @@ async def test_a_failing_weekly_summary_does_not_unmark_the_day(
|
|||||||
await super().send_text(text, recipient)
|
await super().send_text(text, recipient)
|
||||||
|
|
||||||
transport = RefuseSecond()
|
transport = RefuseSecond()
|
||||||
client = mock_client(lambda r: httpx.Response(200))
|
await _at(config, store, transport, directory, sunday).publish_report()
|
||||||
bot = build_bot(
|
|
||||||
config,
|
|
||||||
client,
|
|
||||||
store,
|
|
||||||
transport,
|
|
||||||
directory,
|
|
||||||
Liveness(config, client),
|
|
||||||
clock=lambda: dt.datetime(2026, 8, 31, 21, 37, tzinfo=WARSAW),
|
|
||||||
)
|
|
||||||
await bot.publish_report()
|
|
||||||
assert store.meta(WATCHED_GROUP, LAST_REPORTED_KEY) == sunday
|
assert store.meta(WATCHED_GROUP, LAST_REPORTED_KEY) == sunday
|
||||||
|
|||||||
@ -29,9 +29,11 @@ if TYPE_CHECKING:
|
|||||||
from signal_bot.config import Config
|
from signal_bot.config import Config
|
||||||
from signal_bot.store import Store
|
from signal_bot.store import Store
|
||||||
|
|
||||||
# The report covers the previous full day, so the tests freeze "now" and
|
# The report covers the day it runs on, so the tests freeze "now" and write
|
||||||
# write their events into the day before it.
|
# their events into that same day. YESTERDAY is what the catch-up tests use
|
||||||
|
# for a day the bot still owes.
|
||||||
NOW = dt.datetime(2026, 9, 2, 21, 37, tzinfo=WARSAW)
|
NOW = dt.datetime(2026, 9, 2, 21, 37, tzinfo=WARSAW)
|
||||||
|
TODAY = "2026-09-02"
|
||||||
YESTERDAY = "2026-09-01"
|
YESTERDAY = "2026-09-01"
|
||||||
|
|
||||||
|
|
||||||
@ -43,7 +45,7 @@ def _bot(config: Config, store: Store, transport: FakeTransport, directory: Grou
|
|||||||
|
|
||||||
|
|
||||||
def wrote(
|
def wrote(
|
||||||
store: Store, group_id: str, uuid: str, name: str, times: int, *, day: str = YESTERDAY
|
store: Store, group_id: str, uuid: str, name: str, times: int, *, day: str = TODAY
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Put ``times`` messages from one person into one group on one day."""
|
"""Put ``times`` messages from one person into one group on one day."""
|
||||||
for i in range(times):
|
for i in range(times):
|
||||||
@ -74,17 +76,36 @@ async def test_publish_report_posts_each_group_its_own_tally(
|
|||||||
assert "Alice" not in transport.texts[1]
|
assert "Alice" not in transport.texts[1]
|
||||||
|
|
||||||
|
|
||||||
async def test_the_report_covers_yesterday_not_today(
|
async def test_the_report_covers_today_not_yesterday(
|
||||||
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Today's traffic belongs to tomorrow's report, not tonight's."""
|
"""21:37 posts the day the group is living through, not the one before."""
|
||||||
wrote(store, WATCHED_GROUP, "u1", "Alice", 1, day=YESTERDAY)
|
wrote(store, WATCHED_GROUP, "u1", "Alice", 1, day=TODAY)
|
||||||
wrote(store, WATCHED_GROUP, "u2", "Bob", 9, day="2026-09-02")
|
wrote(store, WATCHED_GROUP, "u2", "Bob", 9, day=YESTERDAY)
|
||||||
await _bot(config, store, transport, directory).publish_report()
|
await _bot(config, store, transport, directory).publish_report()
|
||||||
assert "Alice" in transport.texts[0]
|
assert "Alice" in transport.texts[0]
|
||||||
assert "Bob" not in transport.texts[0]
|
assert "Bob" not in transport.texts[0]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_a_still_open_day_says_when_its_count_stopped(
|
||||||
|
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
||||||
|
) -> None:
|
||||||
|
"""A partial day must not be presented as a whole one."""
|
||||||
|
wrote(store, WATCHED_GROUP, "u1", "Alice", 1)
|
||||||
|
await _bot(config, store, transport, directory).publish_report()
|
||||||
|
assert "licznik zamknięty o 21:37" in transport.texts[0]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_a_caught_up_day_carries_no_closing_caveat(
|
||||||
|
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
||||||
|
) -> None:
|
||||||
|
"""That day is over; its count is complete and needs no excuse."""
|
||||||
|
wrote(store, WATCHED_GROUP, "u1", "Alice", 1, day=YESTERDAY)
|
||||||
|
store.set_meta(WATCHED_GROUP, LAST_REPORTED_KEY, "2026-08-31")
|
||||||
|
await _bot(config, store, transport, directory).publish_report()
|
||||||
|
assert "licznik zamknięty" not in transport.texts[0]
|
||||||
|
|
||||||
|
|
||||||
async def test_nothing_is_deleted_after_publishing(
|
async def test_nothing_is_deleted_after_publishing(
|
||||||
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -93,7 +114,7 @@ async def test_nothing_is_deleted_after_publishing(
|
|||||||
bot = _bot(config, store, transport, directory)
|
bot = _bot(config, store, transport, directory)
|
||||||
await bot.publish_report()
|
await bot.publish_report()
|
||||||
first = transport.texts[0]
|
first = transport.texts[0]
|
||||||
store.set_meta(WATCHED_GROUP, LAST_REPORTED_KEY, "2026-08-31")
|
store.set_meta(WATCHED_GROUP, LAST_REPORTED_KEY, YESTERDAY)
|
||||||
await bot.publish_report()
|
await bot.publish_report()
|
||||||
assert transport.texts[1] == first
|
assert transport.texts[1] == first
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
|||||||
GROUP = "group-a=="
|
GROUP = "group-a=="
|
||||||
|
|
||||||
|
|
||||||
def test_a_group_with_no_marker_gets_only_yesterday(store: Store) -> None:
|
def test_a_group_with_no_marker_gets_only_the_latest_day(store: Store) -> None:
|
||||||
"""A new group, and the first run after this release, must not backlog."""
|
"""A new group, and the first run after this release, must not backlog."""
|
||||||
assert days_to_report(store, GROUP, "2026-09-01") == ["2026-09-01"]
|
assert days_to_report(store, GROUP, "2026-09-01") == ["2026-09-01"]
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ def test_catch_up_is_bounded(store: Store) -> None:
|
|||||||
assert days[0] == "2026-08-26"
|
assert days[0] == "2026-08-26"
|
||||||
|
|
||||||
|
|
||||||
def test_a_marker_ahead_of_yesterday_owes_nothing(store: Store) -> None:
|
def test_a_marker_ahead_of_the_latest_day_owes_nothing(store: Store) -> None:
|
||||||
"""Defends the clock going backwards, e.g. an NTP correction."""
|
"""Defends the clock going backwards, e.g. an NTP correction."""
|
||||||
mark_reported(store, GROUP, "2026-09-05")
|
mark_reported(store, GROUP, "2026-09-05")
|
||||||
assert days_to_report(store, GROUP, "2026-09-01") == []
|
assert days_to_report(store, GROUP, "2026-09-01") == []
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user