Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e759-301a-78b1-bbc1-040ef094b12d
97 lines
4.9 KiB
Python
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'
|