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'