214 lines
7.3 KiB
Python
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"]]
|