Files
rpa_vision_v3/tests/unit/test_competence_promotions.py

214 lines
7.3 KiB
Python

from datetime import datetime, timezone
from uuid import uuid4
import pytest
from core.competences.promotions import (
CompetencePromotionError,
iter_competence_promotions,
promote_competence_from_verdicts,
summarize_competence_promotions,
)
from core.competences.verdicts import store_competence_verdict
def _write_competence(root, state="observed", competence_id="demo_competence"):
state_dir = root / state
state_dir.mkdir(parents=True, exist_ok=True)
path = state_dir / f"{competence_id}.yaml"
path.write_text(
"\n".join([
"schema_version: 1",
f"id: {competence_id}",
"name: Demo competence",
f"learning_state: {state}",
"intent:",
" fr: demo competence",
"methods: []",
"promotion:",
" candidate_requires:",
" - cleaned_segment_validated",
" stable_requires:",
" min_successes: 3",
" distinct_contexts: 3",
" max_unexplained_failures: 0",
"generalisation:",
" seen_contexts: []",
"failure_log: []",
"created_at: '2026-05-29T00:00:00+00:00'",
"last_updated_at: '2026-05-29T00:00:00+00:00'",
]),
encoding="utf-8",
)
return path
def _valid_verdict(root, log_path, competence_id="demo_competence", machine="machine_a"):
verdict_id = str(uuid4())
return store_competence_verdict(
competence_id,
{
"verdict_id": verdict_id,
"verdict_kind": "valid",
"verdict_by": "human:dom",
"workflow_id": f"workflow_{machine}",
"step_results": [
{"step_id": "step_1", "action_type": "keyboard_shortcut", "status": "success"}
],
"context_signature": {
"machine_id": machine,
"screen_state_initial": f"before_{machine}",
"screen_state_after_action": f"after_{machine}",
},
},
competence_root=root,
log_path=log_path,
now=datetime(2026, 5, 29, 17, 0, tzinfo=timezone.utc),
)
def test_promote_competence_dry_run_does_not_write_yaml(tmp_path):
root = tmp_path / "competences"
source = _write_competence(root, state="observed")
verdict_log = tmp_path / "verdicts.jsonl"
verdict = _valid_verdict(root, verdict_log)
before = source.read_text(encoding="utf-8")
result = promote_competence_from_verdicts(
"demo_competence",
{
"promotion_id": str(uuid4()),
"target_state": "candidate",
"verdict_ids": [verdict["verdict_id"]],
"confirmed_by": "human:dom",
"dry_run": True,
},
competence_root=root,
verdict_log_path=verdict_log,
promotion_log_path=tmp_path / "promotions.jsonl",
now=datetime(2026, 5, 29, 18, 0, tzinfo=timezone.utc),
)
assert result["dry_run"] is True
assert result["eligible"] is True
assert result["write_applied"] is False
assert "learning_state: candidate" in result["yaml_diff"]
assert source.read_text(encoding="utf-8") == before
def test_promote_competence_confirm_writes_backup_and_audit(tmp_path):
root = tmp_path / "competences"
source = _write_competence(root, state="observed")
verdict_log = tmp_path / "verdicts.jsonl"
promotion_log = tmp_path / "promotions.jsonl"
verdict = _valid_verdict(root, verdict_log)
promotion_id = str(uuid4())
dry_run = promote_competence_from_verdicts(
"demo_competence",
{
"promotion_id": promotion_id,
"target_state": "candidate",
"verdict_ids": [verdict["verdict_id"]],
"confirmed_by": "human:dom",
"dry_run": True,
},
competence_root=root,
verdict_log_path=verdict_log,
promotion_log_path=promotion_log,
now=datetime(2026, 5, 29, 18, 0, tzinfo=timezone.utc),
)
result = promote_competence_from_verdicts(
"demo_competence",
{
"promotion_id": promotion_id,
"target_state": "candidate",
"verdict_ids": [verdict["verdict_id"]],
"confirmed_by": "human:dom",
"dry_run": False,
"dry_run_token": dry_run["dry_run_token"],
},
competence_root=root,
verdict_log_path=verdict_log,
promotion_log_path=promotion_log,
now=datetime(2026, 5, 29, 18, 0, tzinfo=timezone.utc),
)
target = root / "candidate" / "demo_competence.yaml"
assert result["write_applied"] is True
assert target.exists()
assert not source.exists()
written = target.read_text(encoding="utf-8")
assert "learning_state: candidate" in written
assert verdict["verdict_id"] in written
assert list(source.parent.glob("demo_competence.yaml.*.bak"))
promotions = iter_competence_promotions(log_path=promotion_log)
assert promotions[0]["promotion_id"] == promotion_id
assert promotions[0]["yaml_write"] is True
def test_promote_competence_requires_prior_dry_run_token(tmp_path):
root = tmp_path / "competences"
_write_competence(root, state="observed")
verdict_log = tmp_path / "verdicts.jsonl"
verdict = _valid_verdict(root, verdict_log)
with pytest.raises(CompetencePromotionError, match="dry_run_token"):
promote_competence_from_verdicts(
"demo_competence",
{
"promotion_id": str(uuid4()),
"target_state": "candidate",
"verdict_ids": [verdict["verdict_id"]],
"confirmed_by": "human:dom",
"dry_run": False,
},
competence_root=root,
verdict_log_path=verdict_log,
promotion_log_path=tmp_path / "promotions.jsonl",
)
def test_stable_promotion_requires_three_distinct_contexts(tmp_path):
root = tmp_path / "competences"
_write_competence(root, state="candidate")
verdict_log = tmp_path / "verdicts.jsonl"
verdict = _valid_verdict(root, verdict_log, machine="machine_a")
result = promote_competence_from_verdicts(
"demo_competence",
{
"promotion_id": str(uuid4()),
"target_state": "stable",
"verdict_ids": [verdict["verdict_id"]],
"confirmed_by": "human:dom",
"dry_run": True,
},
competence_root=root,
verdict_log_path=verdict_log,
promotion_log_path=tmp_path / "promotions.jsonl",
)
assert result["eligible"] is False
assert any("3 verdicts valid requis" in reason for reason in result["blocking_reasons"])
assert any("3 contextes distincts requis" in reason for reason in result["blocking_reasons"])
def test_summarize_competence_promotions_reports_eligible_candidate(tmp_path):
root = tmp_path / "competences"
_write_competence(root, state="observed")
verdict_log = tmp_path / "verdicts.jsonl"
verdict = _valid_verdict(root, verdict_log)
summary = summarize_competence_promotions(
competence_root=root,
verdict_log_path=verdict_log,
)
assert summary[0]["id"] == "demo_competence"
assert summary[0]["verdict_counts"]["valid"] == 1
candidate = summary[0]["eligible_targets"]["candidate"]
assert candidate["eligible"] is True
assert candidate["recommended_verdict_ids"] == [verdict["verdict_id"]]