2026-07-25 05:39:44 +02:00
|
|
|
"""tests/unit/foundation/test_board_programs.py"""
|
|
|
|
|
|
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from roboco.config import Settings
|
|
|
|
|
from roboco.foundation.policy.board_programs import (
|
|
|
|
|
PROGRAMS,
|
2026-07-25 17:13:32 +02:00
|
|
|
WEEK_SECONDS,
|
2026-07-25 05:39:44 +02:00
|
|
|
BoardProgram,
|
|
|
|
|
TriggerKind,
|
|
|
|
|
program_due,
|
|
|
|
|
project_participates,
|
|
|
|
|
validate_board_programs_field,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_x_feature_default_interval_matches_settings_field_default() -> None:
|
|
|
|
|
"""Guards the registry cadence and the live due-check's cadence
|
|
|
|
|
(``Settings.x_feature_spotlight_interval_seconds``) from drifting apart
|
|
|
|
|
again — reads the pydantic field default, never a live env-configured
|
|
|
|
|
instance, so this can't pass by accident in a differently-configured
|
|
|
|
|
environment."""
|
|
|
|
|
field_default = Settings.model_fields["x_feature_spotlight_interval_seconds"]
|
|
|
|
|
assert PROGRAMS["x_feature"].default_interval_seconds == field_default.default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_the_two_migrated_programs() -> None:
|
2026-07-25 17:13:32 +02:00
|
|
|
assert {"roadmap", "x_feature"} <= set(PROGRAMS)
|
2026-07-25 05:39:44 +02:00
|
|
|
rm = PROGRAMS["roadmap"]
|
|
|
|
|
assert rm.role == "product_owner"
|
|
|
|
|
assert rm.source == "board_roadmap"
|
|
|
|
|
assert rm.trigger is TriggerKind.CRON
|
|
|
|
|
xf = PROGRAMS["x_feature"]
|
|
|
|
|
assert xf.role == "head_marketing"
|
|
|
|
|
assert xf.source == "x_feature_exploration"
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 17:13:32 +02:00
|
|
|
_PEST_CONTROL_MAX_ITEMS_PER_CYCLE = 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_pest_control() -> None:
|
|
|
|
|
pc = PROGRAMS["pest_control"]
|
|
|
|
|
assert pc.role == "product_owner"
|
|
|
|
|
assert pc.source == "board_pest_control"
|
|
|
|
|
assert pc.trigger is TriggerKind.CRON
|
|
|
|
|
assert pc.scope == "project"
|
|
|
|
|
assert pc.max_items_per_cycle == _PEST_CONTROL_MAX_ITEMS_PER_CYCLE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_periscope() -> None:
|
|
|
|
|
p = PROGRAMS["periscope"]
|
|
|
|
|
assert p.role == "head_marketing"
|
|
|
|
|
assert p.source == "board_periscope"
|
|
|
|
|
assert p.trigger is TriggerKind.CRON
|
|
|
|
|
assert p.scope == "org"
|
|
|
|
|
assert p.default_interval_seconds == WEEK_SECONDS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SCALES_MAX_ITEMS_PER_CYCLE = 7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_scales() -> None:
|
|
|
|
|
s = PROGRAMS["scales"]
|
|
|
|
|
assert s.role == "product_owner"
|
|
|
|
|
assert s.source == "board_scales"
|
|
|
|
|
assert s.trigger is TriggerKind.CRON
|
|
|
|
|
assert s.scope == "org"
|
|
|
|
|
assert s.max_items_per_cycle == _SCALES_MAX_ITEMS_PER_CYCLE
|
|
|
|
|
assert s.default_interval_seconds == 30 * 24 * 3600
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_coroner() -> None:
|
|
|
|
|
c = PROGRAMS["coroner"]
|
|
|
|
|
assert c.role == "auditor"
|
|
|
|
|
assert c.source == "board_coroner"
|
|
|
|
|
assert c.trigger is TriggerKind.EVENT
|
|
|
|
|
assert c.scope == "org"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_coroner_is_never_cron_due() -> None:
|
|
|
|
|
"""An EVENT program is never cron-due regardless of how long it's been
|
|
|
|
|
since the last cycle opened — mirrors test_program_due_event_never_cron_
|
|
|
|
|
fires but against the real registered entry."""
|
|
|
|
|
assert not program_due(
|
|
|
|
|
PROGRAMS["coroner"],
|
|
|
|
|
now=datetime(2026, 7, 24, tzinfo=UTC),
|
|
|
|
|
last_opened_at=None,
|
|
|
|
|
interval_override=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_sentinel() -> None:
|
|
|
|
|
s = PROGRAMS["sentinel"]
|
|
|
|
|
assert s.role == "auditor"
|
|
|
|
|
assert s.source == "board_sentinel"
|
|
|
|
|
assert s.trigger is TriggerKind.CRON
|
|
|
|
|
assert s.scope == "org"
|
|
|
|
|
assert s.default_interval_seconds == WEEK_SECONDS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SPACKLE_MAX_ITEMS_PER_CYCLE = 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_spackle() -> None:
|
|
|
|
|
sp = PROGRAMS["spackle"]
|
|
|
|
|
assert sp.role == "product_owner"
|
|
|
|
|
assert sp.source == "board_spackle"
|
|
|
|
|
assert sp.trigger is TriggerKind.CRON
|
|
|
|
|
assert sp.scope == "project"
|
|
|
|
|
assert sp.max_items_per_cycle == _SPACKLE_MAX_ITEMS_PER_CYCLE
|
|
|
|
|
assert sp.default_interval_seconds == 2 * WEEK_SECONDS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_MIRROR_MAX_ITEMS_PER_CYCLE = 5
|
|
|
|
|
_QUARTER_SECONDS = 90 * 24 * 3600
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_mirror() -> None:
|
|
|
|
|
m = PROGRAMS["mirror"]
|
|
|
|
|
assert m.role == "head_marketing"
|
|
|
|
|
assert m.source == "board_mirror"
|
|
|
|
|
assert m.trigger is TriggerKind.CRON
|
|
|
|
|
assert m.scope == "project"
|
|
|
|
|
assert m.max_items_per_cycle == _MIRROR_MAX_ITEMS_PER_CYCLE
|
|
|
|
|
assert m.default_interval_seconds == _QUARTER_SECONDS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_megaphone() -> None:
|
|
|
|
|
mg = PROGRAMS["megaphone"]
|
|
|
|
|
assert mg.role == "head_marketing"
|
|
|
|
|
assert mg.source == "board_megaphone"
|
|
|
|
|
assert mg.trigger is TriggerKind.CRON
|
|
|
|
|
assert mg.scope == "org"
|
|
|
|
|
assert mg.default_interval_seconds == 3 * 24 * 3600
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_LIBRARIAN_MAX_ITEMS_PER_CYCLE = 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_librarian() -> None:
|
|
|
|
|
p = PROGRAMS["librarian"]
|
|
|
|
|
assert p.role == "auditor"
|
|
|
|
|
assert p.source == "board_librarian"
|
|
|
|
|
assert p.trigger is TriggerKind.CRON
|
|
|
|
|
assert p.scope == "org"
|
|
|
|
|
assert p.max_items_per_cycle == _LIBRARIAN_MAX_ITEMS_PER_CYCLE
|
|
|
|
|
assert p.default_interval_seconds == 2 * WEEK_SECONDS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_WAR_ROOM_MAX_POSTS_PER_CAMPAIGN = 6
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_war_room() -> None:
|
|
|
|
|
wr = PROGRAMS["war_room"]
|
|
|
|
|
assert wr.role == "head_marketing"
|
|
|
|
|
assert wr.source == "board_war_room"
|
|
|
|
|
assert wr.trigger is TriggerKind.EVENT
|
|
|
|
|
assert wr.scope == "org"
|
|
|
|
|
assert wr.max_items_per_cycle == _WAR_ROOM_MAX_POSTS_PER_CAMPAIGN
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_war_room_is_never_cron_due() -> None:
|
|
|
|
|
"""An EVENT program is never cron-due regardless of how long it's been
|
|
|
|
|
since the last cycle opened — mirrors test_coroner_is_never_cron_due.
|
|
|
|
|
Unlike Coroner, War Room's ``_ORIGINATORS`` entry is a REAL originator
|
|
|
|
|
(see test_board_program_engine.py), so this test is what actually proves
|
|
|
|
|
the loop still never drives it — the trigger-kind guard, not a stub."""
|
|
|
|
|
assert not program_due(
|
|
|
|
|
PROGRAMS["war_room"],
|
|
|
|
|
now=datetime(2026, 7, 24, tzinfo=UTC),
|
|
|
|
|
last_opened_at=None,
|
|
|
|
|
interval_override=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_DOGFOOD_MAX_ITEMS_PER_CYCLE = 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_dogfood() -> None:
|
|
|
|
|
d = PROGRAMS["dogfood"]
|
|
|
|
|
assert d.role == "product_owner"
|
|
|
|
|
assert d.source == "board_dogfood"
|
|
|
|
|
assert d.trigger is TriggerKind.EVENT
|
|
|
|
|
assert d.scope == "project"
|
|
|
|
|
assert d.max_items_per_cycle == _DOGFOOD_MAX_ITEMS_PER_CYCLE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_dogfood_is_never_cron_due() -> None:
|
|
|
|
|
"""An EVENT program is never cron-due regardless of how long it's been
|
|
|
|
|
since the last cycle opened — mirrors test_coroner_is_never_cron_due,
|
|
|
|
|
but against Dogfood: unlike Coroner it DOES have a real originator (see
|
|
|
|
|
roboco.services.board_programs._originate_dogfood), so this asserts the
|
|
|
|
|
cron loop's own gate refuses it, not that no originator exists."""
|
|
|
|
|
assert not program_due(
|
|
|
|
|
PROGRAMS["dogfood"],
|
|
|
|
|
now=datetime(2026, 7, 24, tzinfo=UTC),
|
|
|
|
|
last_opened_at=None,
|
|
|
|
|
interval_override=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_BARFLY_MAX_ITEMS_PER_CYCLE = 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_barfly() -> None:
|
|
|
|
|
b = PROGRAMS["barfly"]
|
|
|
|
|
assert b.role == "head_marketing"
|
|
|
|
|
assert b.source == "board_barfly"
|
|
|
|
|
assert b.trigger is TriggerKind.CRON
|
|
|
|
|
assert b.scope == "org"
|
|
|
|
|
assert b.max_items_per_cycle == _BARFLY_MAX_ITEMS_PER_CYCLE
|
|
|
|
|
assert b.default_interval_seconds == 2 * 24 * 3600
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_carries_fourteen_programs() -> None:
|
|
|
|
|
"""Locks the union so a future addition/removal is deliberate — matches
|
|
|
|
|
the count-whatever-your-base-has-plus-war_room shape the other
|
|
|
|
|
registry-parity tests already exercise per-key."""
|
|
|
|
|
assert set(PROGRAMS) == {
|
|
|
|
|
"roadmap",
|
|
|
|
|
"x_feature",
|
|
|
|
|
"pest_control",
|
|
|
|
|
"periscope",
|
|
|
|
|
"coroner",
|
|
|
|
|
"sentinel",
|
|
|
|
|
"spackle",
|
|
|
|
|
"scales",
|
|
|
|
|
"mirror",
|
|
|
|
|
"megaphone",
|
|
|
|
|
"librarian",
|
|
|
|
|
"war_room",
|
|
|
|
|
"barfly",
|
|
|
|
|
"dogfood",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_MIN_DESCRIPTION_CHARS = 40
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_every_registry_entry_has_human_title_and_description() -> None:
|
|
|
|
|
"""The panel renders title/description, never the raw key — a registry
|
|
|
|
|
entry shipping without them regresses the card to unreadable keys."""
|
|
|
|
|
for key, program in PROGRAMS.items():
|
|
|
|
|
assert program.title.strip(), f"{key} has no title"
|
|
|
|
|
assert len(program.description.strip()) >= _MIN_DESCRIPTION_CHARS, (
|
|
|
|
|
f"{key} description too thin"
|
|
|
|
|
)
|
|
|
|
|
titles = [p.title for p in PROGRAMS.values()]
|
|
|
|
|
assert len(set(titles)) == len(titles), "duplicate program titles"
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 05:39:44 +02:00
|
|
|
def test_program_due_cron_interval() -> None:
|
|
|
|
|
now = datetime(2026, 7, 24, tzinfo=UTC)
|
|
|
|
|
p = PROGRAMS["roadmap"]
|
|
|
|
|
assert program_due(p, now=now, last_opened_at=None, interval_override=None)
|
|
|
|
|
recent = now - timedelta(seconds=10)
|
|
|
|
|
assert not program_due(p, now=now, last_opened_at=recent, interval_override=None)
|
|
|
|
|
old = now - timedelta(seconds=p.default_interval_seconds + 1)
|
|
|
|
|
assert program_due(p, now=now, last_opened_at=old, interval_override=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_program_due_event_never_cron_fires() -> None:
|
|
|
|
|
p = BoardProgram(
|
|
|
|
|
key="k",
|
|
|
|
|
role="auditor",
|
|
|
|
|
trigger=TriggerKind.EVENT,
|
|
|
|
|
source="s",
|
|
|
|
|
default_interval_seconds=0,
|
|
|
|
|
)
|
|
|
|
|
assert not program_due(
|
|
|
|
|
p,
|
|
|
|
|
now=datetime(2026, 7, 24, tzinfo=UTC),
|
|
|
|
|
last_opened_at=None,
|
|
|
|
|
interval_override=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_program_due_interval_override_wins_over_default() -> None:
|
|
|
|
|
now = datetime(2026, 7, 24, tzinfo=UTC)
|
|
|
|
|
p = PROGRAMS["roadmap"]
|
|
|
|
|
recent = now - timedelta(seconds=100)
|
|
|
|
|
# Default interval (a week) would still block; a short override fires.
|
|
|
|
|
assert program_due(p, now=now, last_opened_at=recent, interval_override=50)
|
|
|
|
|
assert not program_due(p, now=now, last_opened_at=recent, interval_override=200)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Task 6b: per-project program scoping
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_registry_entries_default_to_org_scope() -> None:
|
|
|
|
|
assert PROGRAMS["roadmap"].scope == "org"
|
|
|
|
|
assert PROGRAMS["x_feature"].scope == "org"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_PROJECT_PROGRAM = BoardProgram(
|
|
|
|
|
key="pest_control",
|
|
|
|
|
role="product_owner",
|
|
|
|
|
trigger=TriggerKind.CRON,
|
|
|
|
|
source="board_pest_control",
|
|
|
|
|
default_interval_seconds=7 * 24 * 3600,
|
|
|
|
|
scope="project",
|
|
|
|
|
)
|
|
|
|
|
_ORG_PROGRAM = PROGRAMS["roadmap"] # scope="org"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_project_scoped_program_is_affirmative_opt_in() -> None:
|
|
|
|
|
assert not project_participates(_PROJECT_PROGRAM, None)
|
|
|
|
|
assert not project_participates(_PROJECT_PROGRAM, [])
|
|
|
|
|
assert not project_participates(_PROJECT_PROGRAM, ["some_other_key"])
|
|
|
|
|
assert project_participates(_PROJECT_PROGRAM, ["pest_control"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_org_scoped_program_is_default_eligible_opt_out() -> None:
|
|
|
|
|
assert project_participates(_ORG_PROGRAM, None)
|
|
|
|
|
assert project_participates(_ORG_PROGRAM, [])
|
|
|
|
|
assert project_participates(_ORG_PROGRAM, ["some_other_key"])
|
|
|
|
|
assert not project_participates(_ORG_PROGRAM, ["!roadmap"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_board_programs_field_accepts_none() -> None:
|
|
|
|
|
assert validate_board_programs_field(None) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_board_programs_field_accepts_known_org_exclusion() -> None:
|
|
|
|
|
assert validate_board_programs_field(["!roadmap"]) == ["!roadmap"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_board_programs_field_rejects_plain_key_on_org_scoped_program() -> (
|
|
|
|
|
None
|
|
|
|
|
):
|
|
|
|
|
"""Both registered programs are org-scoped today, so a plain "roadmap"
|
|
|
|
|
entry (the project-scoped opt-in form) is meaningless — org-scoped
|
|
|
|
|
programs run against every project by default and are only ever
|
|
|
|
|
excluded via '!key'. See test_validate_board_programs_field_allows_
|
|
|
|
|
plain_project_scoped_key below for the positive case on a synthetic
|
|
|
|
|
project-scoped program."""
|
|
|
|
|
with pytest.raises(ValueError, match="meaningless"):
|
|
|
|
|
validate_board_programs_field(["roadmap"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_board_programs_field_rejects_unknown_key() -> None:
|
|
|
|
|
with pytest.raises(ValueError, match="unknown board program key"):
|
|
|
|
|
validate_board_programs_field(["not_a_real_program"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_board_programs_field_rejects_unknown_excluded_key() -> None:
|
|
|
|
|
with pytest.raises(ValueError, match="unknown board program key"):
|
|
|
|
|
validate_board_programs_field(["!not_a_real_program"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_board_programs_field_rejects_bang_on_project_scoped_key() -> None:
|
|
|
|
|
registry = {"pest_control": _PROJECT_PROGRAM}
|
|
|
|
|
with pytest.raises(ValueError, match="meaningless"):
|
|
|
|
|
validate_board_programs_field(["!pest_control"], programs=registry)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_board_programs_field_allows_plain_project_scoped_key() -> None:
|
|
|
|
|
registry = {"pest_control": _PROJECT_PROGRAM}
|
|
|
|
|
assert validate_board_programs_field(["pest_control"], programs=registry) == [
|
|
|
|
|
"pest_control"
|
|
|
|
|
]
|