From f0300f0ae05407fd4ea8ed54969cf225a3c4bb24 Mon Sep 17 00:00:00 2001 From: ozand Date: Sat, 2 May 2026 20:11:26 +0300 Subject: [PATCH] fix: treat accepted promotions as replay resolved --- .../src/nanobot_ops_dashboard/app.py | 26 ++++++++++++ .../tests/test_dashboard_truth_audit_gaps.py | 42 ++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/ops/dashboard/src/nanobot_ops_dashboard/app.py b/ops/dashboard/src/nanobot_ops_dashboard/app.py index 03f6cc6..a86a1c2 100644 --- a/ops/dashboard/src/nanobot_ops_dashboard/app.py +++ b/ops/dashboard/src/nanobot_ops_dashboard/app.py @@ -93,6 +93,32 @@ def _promotion_replay_readiness_from_promotions(promotions: list[dict] | None) - readiness_checks = detail.get('readiness_checks') or detail.get('readinessChecks') readiness_reasons = detail.get('readiness_reasons') or detail.get('readinessReasons') or [] missing_records = [name for name, value in {'decision_record': decision_record, 'accepted_record': accepted_record}.items() if _missing_record(value)] + accepted_lifecycle = ( + row.get('lifecycle_phase') == 'accepted' + or row.get('status') == 'accept' + or review_packet_status == 'accepted' + or decision == 'accept' + ) + if accepted_lifecycle and not _missing_record(decision_record) and not _missing_record(accepted_record): + return { + 'schema_version': 'promotion-replay-readiness-v1', + 'state': 'accepted', + 'reason': 'promotion_candidate_accepted', + 'promotion_id': row.get('identity_key') or row.get('title'), + 'status': row.get('status'), + 'review_status': review_status, + 'decision': decision, + 'review_packet_status': review_packet_status or 'accepted', + 'decision_record': decision_record, + 'accepted_record': accepted_record, + 'missing_records': [], + 'readiness_checks': readiness_checks, + 'readiness_reasons': readiness_reasons, + 'recommended_next_action': detail.get('recommended_next_action'), + 'candidate_path': detail.get('candidate_path'), + 'artifact_path': detail.get('artifact_path'), + 'collected_at': row.get('collected_at'), + } ready_for_policy_review = ( review_status == 'ready_for_policy_review' or decision == 'ready_for_policy_review' diff --git a/ops/dashboard/tests/test_dashboard_truth_audit_gaps.py b/ops/dashboard/tests/test_dashboard_truth_audit_gaps.py index 6e6f7ca..f795918 100644 --- a/ops/dashboard/tests/test_dashboard_truth_audit_gaps.py +++ b/ops/dashboard/tests/test_dashboard_truth_audit_gaps.py @@ -8,7 +8,7 @@ from wsgiref.util import setup_testing_defaults from nanobot_ops_dashboard import app as dashboard_app -from nanobot_ops_dashboard.app import create_app, _dashboard_runtime_parity, _selected_hypothesis_terminal_evidence, _material_progress_summary, _approval_snapshot, _autonomy_verdict, _ambition_utilization_verdict, _experiment_snapshot_from_payload, _discover_subagent_requests +from nanobot_ops_dashboard.app import create_app, _dashboard_runtime_parity, _selected_hypothesis_terminal_evidence, _material_progress_summary, _approval_snapshot, _autonomy_verdict, _ambition_utilization_verdict, _experiment_snapshot_from_payload, _discover_subagent_requests, _promotion_replay_readiness_from_promotions from nanobot_ops_dashboard.config import DashboardConfig from nanobot_ops_dashboard.storage import init_db, insert_collection, upsert_event @@ -76,6 +76,46 @@ def _seed_hypotheses_backlog(repo_root: Path, *, entry_count: int, selected_id: return backlog +def test_promotion_replay_readiness_treats_accepted_promotion_as_resolved() -> None: + readiness = _promotion_replay_readiness_from_promotions([ + { + 'identity_key': 'promotion-a4c72dd77e02', + 'status': 'accept', + 'lifecycle_phase': 'accepted', + 'replay_readiness': 'blocked', + 'source': 'eeepc', + 'collected_at': '2026-05-02T17:03:18.133759Z', + 'detail': { + 'candidate_path': '/var/lib/eeepc-agent/self-evolving-agent/state/promotions/promotion-a4c72dd77e02.json', + 'artifact_path': '/var/lib/eeepc-agent/self-evolving-agent/state/improvements/materialized-cycle-bcb6c4e2203a.json', + 'decision_record': '/var/lib/eeepc-agent/self-evolving-agent/state/promotions/decisions/promotion-a4c72dd77e02.json', + 'accepted_record': '/var/lib/eeepc-agent/self-evolving-agent/state/promotions/accepted/promotion-a4c72dd77e02.json', + 'readiness_blocker': None, + 'recommended_next_action': None, + 'governance_packet': { + 'review_packet_status': 'accepted', + 'review_status': 'reviewed', + 'decision': 'accept', + 'decision_record': '/var/lib/eeepc-agent/self-evolving-agent/state/promotions/decisions/promotion-a4c72dd77e02.json', + 'accepted_record': '/var/lib/eeepc-agent/self-evolving-agent/state/promotions/accepted/promotion-a4c72dd77e02.json', + }, + }, + } + ]) + + assert readiness is not None + assert readiness['promotion_id'] == 'promotion-a4c72dd77e02' + assert readiness['state'] == 'accepted' + assert readiness['reason'] == 'promotion_candidate_accepted' + assert readiness['status'] == 'accept' + assert readiness['review_packet_status'] == 'accepted' + assert readiness['decision'] == 'accept' + assert readiness['decision_record'].endswith('/decisions/promotion-a4c72dd77e02.json') + assert readiness['accepted_record'].endswith('/accepted/promotion-a4c72dd77e02.json') + assert readiness['missing_records'] == [] + assert readiness['recommended_next_action'] is None + + def test_material_progress_compacts_recursive_selfevo_lifecycle_evidence() -> None: recursive_issue = { 'number': 82,