activity-core/tests/test_pqrst_monthly.py
tegwick ce9a8f8edf
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 6s
Build and Publish Container Image / build-and-push (push) Successful in 51s
Enable verified monthly PQRST schedule and retain CI coverage
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e759-301a-78b1-bbc1-040ef094b12d
2026-09-28 12:18:51 +02:00

97 lines
4.9 KiB
Python

from datetime import datetime, timezone
import importlib.util
import json
from pathlib import Path
import pytest
# Installed tests use the actual registered resolver.
from activity_core.context_resolvers import pqrst as p
NOW = datetime(2026, 10, 1, 7, 30, tzinfo=timezone.utc)
def entry(day='2026-09-15', values=(30, 20, 25, 0, 25), identity='one', extra='', front=None):
sig = ' '.join(k + str(v) for k, v in zip('PQRST', values))
block = 'PQRST-Estimate\n' + ''.join(f'{k}: {v}%\n' for k, v in zip('PQRST', values))
block += f'Sum: 100%\nConfidence: medium\nSignature: {sig}\nDominant factors: Changed parser and tested invalid input.\n'
return f'---\nid: {identity}\nworker_kind: agent-session\nstatus: draft\nrecorded_at: "{day}"\npqrst_estimate: "{front or sig}"\n---\n## PQRST estimate\n{extra}```text\n{block}```\n'
def test_equal_entry_means_deltas_and_calendar_gaps():
rows = [('a', entry('2026-08-10', (20, 20, 40, 0, 20), 'a')),
('b', entry(identity='b')), ('c', entry(values=(40, 10, 30, 10, 10), identity='c')),
('future', entry('2026-10-01', identity='f'))]
r = p.summarize(rows, 'a'*40, NOW)
assert r['months'][-1]['n'] == 2
assert r['months'][-1]['mean_pp'] == [35, 15, 27.5, 5, 17.5]
assert r['months'][-1]['change_pp'] == [15, -5, -12.5, 5, -2.5]
assert r['months'][0]['mean_pp'] is None and r['months'][0]['change_pp'] is None
assert r['counts']['outside_window'] == 1
assert r['months'][-1]['drafts'] == 2
assert len(json.dumps(r, separators=(',', ':'))) < 3900
def test_failed_attempt_not_counted_or_mixed_with_selected_retry():
bad = '```text\nPQRST-Estimate\nP: 30%\nQ: 15%\nR: 25%\nS: 0%\nT: 30%\nSum: 100%\nConfidence: medium\nSignature: P30 Q30 R25 S0 T30\n```\n'
r = p.summarize([('a', entry(extra=bad))], 'a'*40, NOW)['months'][-1]
assert r['n'] == 1 and r['attempts'] == 2 and r['mean_pp'] == [30, 20, 25, 0, 25]
@pytest.mark.parametrize('change', [
lambda s:s.replace('Q: 20%', 'Q: 21%'),
lambda s:s.replace('Confidence: medium\n',''),
lambda s:s.replace('Confidence: medium','Confidence: confident'),
lambda s:s.replace('Dominant factors: Changed parser and tested invalid input.','Dominant factors:'),
lambda s:s.replace('P: 30%', 'P: 30%\nP: 30%'),
lambda s:s.replace('Sum: 100%', 'Sum: 100%\nU: 0%'),
lambda s:s.replace('P: 30%', 'P: -30%'),
])
def test_invalid_excluded_without_zero_imputation(change):
r = p.summarize([('a', change(entry()))], 'a'*40, NOW)['months'][-1]
assert r['invalid'] == 1 and r['n'] == 0 and r['mean_pp'] is None
def test_duplicate_ids_and_ambiguous_attempts_are_quarantined():
r = p.summarize([('a', entry()), ('b', entry())], 'a'*40, NOW)['months'][-1]
assert r['invalid'] == 2 and r['n'] == 0
block = entry().split('## PQRST estimate\n')[1]
assert p.summarize([('a', entry(extra=block))], 'a'*40, NOW)['months'][-1]['invalid'] == 1
def test_date_timezone_and_year_rollover():
r = p.summarize([('a', entry('2026-08-31T23:30:00Z'))], 'a'*40, NOW)
assert r['months'][-1]['n'] == 1
assert p.month_keys(datetime(2027, 1, 1, tzinfo=timezone.utc))[-1] == '2026-12'
assert p.month_keys(datetime(2026, 9, 30, 22, tzinfo=timezone.utc))[-1] == '2026-09'
def test_missing_dates_and_missing_records_are_visible():
a=entry().replace('recorded_at: "2026-09-15"','recorded_at: missing')
b=entry(identity='b').split('## PQRST estimate')[0].replace('pqrst_estimate: "P30 Q20 R25 S0 T25"\n','')
r=p.summarize([('a',a),('b',b)],'a'*40,NOW)
assert r['counts']['unclassifiable']==1 and r['status']=='needs_review'
assert r['months'][-1]['missing']==1
def test_unavailable_does_not_publish_partial_or_zero_means(monkeypatch):
monkeypatch.setattr(p,'collect',lambda: (_ for _ in ()).throw(ValueError('truncated')))
r=json.loads(p.PqrstResolver().resolve('monthly',None,{}))
assert r['status']=='unavailable' and r['months']==[] and r['model_calls']==0
def test_projected_definition_uses_report_only_native_schedule(tmp_path):
import yaml
from activity_core.definition_parser import parse_file
root = Path(__file__).resolve().parents[1]
docs = list(yaml.safe_load_all((root / 'k8s/railiance/20-runtime.yaml').read_text()))
cm = next(x for x in docs if x and x['kind'] == 'ConfigMap' and x['metadata']['name'] == 'actcore-external-activity-definitions')
path = tmp_path / 'monthly-pqrst-review.md'
path.write_text(cm['data']['monthly-pqrst-review.md'])
definition = parse_file(path)
assert definition.trigger_config['cron_expression'] == '30 9 1 * *'
assert definition.trigger_config['timezone'] == 'Europe/Berlin'
assert definition.rules == []
instruction = definition.instructions[0]
assert instruction['model'] == 'deterministic'
assert [sink['type'] for sink in instruction['report_sinks']] == ['working-memory', 'state-hub-progress']
assert instruction['report_sinks'][1]['event_type'] == 'pqrst_monthly_review'