diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 90f6c8c..abed2c2 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -286,7 +286,7 @@ | task | ACTIVITY-WP-ADHOC-2026-09-14-T02 | done | — | workplans/ADHOC-2026-09-14.md | | task | ACTIVITY-WP-ADHOC-2026-10-19-T01 | done | — | workplans/ADHOC-2026-10-19.md | | task | ACTIVITY-WP-ADHOC-2026-10-19-T02 | done | — | workplans/ADHOC-2026-10-19.md | -| task | ACTIVITY-WP-ADHOC-2026-10-19-T03 | todo | — | workplans/ADHOC-2026-10-19.md | +| task | ACTIVITY-WP-ADHOC-2026-10-19-T03 | done | — | workplans/ADHOC-2026-10-19.md | | task | T01 | done | — | workplans/custodian-WP-0001-temporal-backbone.md | | task | T02 | done | — | workplans/custodian-WP-0001-temporal-backbone.md | | task | T03 | done | — | workplans/custodian-WP-0001-temporal-backbone.md | diff --git a/src/activity_core/release_health.py b/src/activity_core/release_health.py index c659ab2..0d2b59b 100644 --- a/src/activity_core/release_health.py +++ b/src/activity_core/release_health.py @@ -15,8 +15,12 @@ FRONTEND = { str(uuid5(NAMESPACE, f'frontend-patterns-{cadence}')): (cadence, cron) for cadence, cron in [('daily', '0 2 * * *'), ('weekly', '0 3 * * 2'), ('monthly', '0 9 1 * *')] } -SINK = {'type': 'state-hub-progress', 'event_type': 'fep_feedback_intake', - 'author': 'activity-core', 'topic_id': 'fe2aaa78-9c20-4feb-b3d2-4fe0529572a3'} +SINKS = { + cadence: {'type': 'state-hub-progress', 'event_type': event_type, + 'author': 'activity-core', 'topic_id': 'fe2aaa78-9c20-4feb-b3d2-4fe0529572a3'} + for cadence, event_type in [('daily', 'fep_feedback_intake'), ('weekly', 'fep_improvement_review'), + ('monthly', 'fep_monthly_review')] +} def definition_matches(row): @@ -29,12 +33,14 @@ def definition_matches(row): and row.trigger_config.get('cron_expression') == cron and row.trigger_config.get('timezone') == 'Europe/Berlin' and row.trigger_config.get('misfire_policy') == 'catchup_latest' - and row.trigger_config.get('catchup_window_seconds') == 86400 + # Absent means the catchup_latest default (24h, schedule_manager); the + # schedule's actual catchup_window is checked separately. + and row.trigger_config.get('catchup_window_seconds') in (None, 86400) and row.trigger_config.get('jitter_seconds', 0) == 0 and isinstance(instructions, list) and len(instructions) == 1 and instructions[0].get('id') == f'frontend-patterns-{cadence}-report' and instructions[0].get('model') == 'deterministic' - and instructions[0].get('report_sinks') == [SINK]) + and instructions[0].get('report_sinks') == [SINKS[cadence]]) def expanded(ranges, minimum, maximum): diff --git a/tests/test_release_health.py b/tests/test_release_health.py index 79f5465..ee27cc2 100644 --- a/tests/test_release_health.py +++ b/tests/test_release_health.py @@ -11,13 +11,13 @@ from temporalio.api.common.v1 import Payloads, WorkflowType from temporalio.api.taskqueue.v1 import TaskQueue from temporalio.api.workflow.v1 import NewWorkflowExecutionInfo -from activity_core.release_health import FRONTEND, SINK, definition_matches, probe, schedule_matches +from activity_core.release_health import FRONTEND, SINKS, definition_matches, probe, schedule_matches def rows(): return [SimpleNamespace(id=UUID(identity),enabled=True,trigger_type='cron', trigger_config={'cron_expression':cron,'timezone':'Europe/Berlin','misfire_policy':'catchup_latest','catchup_window_seconds':86400}, - instructions_json=[{'id':f'frontend-patterns-{cadence}-report','model':'deterministic','report_sinks':[dict(SINK)]}]) + instructions_json=[{'id':f'frontend-patterns-{cadence}-report','model':'deterministic','report_sinks':[dict(SINKS[cadence])]}]) for identity,(cadence,cron) in FRONTEND.items()] @@ -74,7 +74,7 @@ async def test_actual_describe_payload_arguments_are_decoded_with_bound_client_c @pytest.mark.parametrize('field,value',[('enabled',False),('trigger_type','event'), - ('instructions_json',[{'model':'llm','report_sinks':[SINK]}])]) + ('instructions_json',[{'model':'llm','report_sinks':[SINKS['daily']]}])]) async def test_definition_behavior_drift_refused(field,value): data=rows();setattr(data[0],field,value) assert (await probe(*sources(items=data)))['healthy'] is False @@ -87,6 +87,20 @@ def test_trigger_scope_drift_refused(field,value): assert not definition_matches(row) +async def test_live_rows_without_explicit_catchup_window_use_policy_default(): + data=rows() + for row in data:del row.trigger_config['catchup_window_seconds'] + assert (await probe(*sources(items=data)))['healthy'] is True + + +def test_each_cadence_requires_its_own_report_sink(): + for row in rows(): + cadence=FRONTEND[str(row.id)][0] + other=next(c for c in SINKS if c!=cadence) + row.instructions_json[0]['report_sinks']=[dict(SINKS[other])] + assert not definition_matches(row) + + @pytest.mark.parametrize('change',['pause','limit','extra_calendar','interval','skip','cron','queue','args','sink','policy','timezone']) async def test_schedule_and_report_drift_fail_closed(change): data=rows();descriptions={identity:description(identity,True) for identity in FRONTEND} diff --git a/workplans/ADHOC-2026-10-19.md b/workplans/ADHOC-2026-10-19.md index 5501c3a..619c048 100644 --- a/workplans/ADHOC-2026-10-19.md +++ b/workplans/ADHOC-2026-10-19.md @@ -57,7 +57,7 @@ f9f2640 / `sha256:fdc631d7`. Evidence: ```task id: ACTIVITY-WP-ADHOC-2026-10-19-T03 -status: todo +status: done priority: medium state_hub_task_id: "0bfc9af8-1b75-59ab-91e6-cef5b09bc08d" ``` @@ -71,3 +71,17 @@ three frontend-patterns definitions, while weekly and monthly use `fep_improvement_review` and `fep_monthly_review`. Fix the probe to check the per-cadence sinks, and decide whether to run an admin definition sync or accept a missing `catchup_window_seconds` when the schedule enforces it. + +2026-10-20: fixed in `release_health.py`. +- Each cadence now expects its own sink: daily `fep_feedback_intake`, weekly + `fep_improvement_review`, monthly `fep_monthly_review`. All sit on topic + `fe2aaa78` with author activity-core, as in the 2026-10-03 + natural-cadence evidence. +- A row with no `catchup_window_seconds` is accepted, because the + `catchup_latest` policy's default in `schedule_manager` is 24h. An explicit + value other than 86400 is still refused, and the probe still checks the live + Temporal `catchup_window` separately. No admin definition sync is needed. +- Two new tests cover these cases. The full suite passes (877 passed, + 1 skipped). +- Not deployed: the fix ships with the next image release, and live + `/release/health` stays false until then.