OpenSpace/tests/skill_engine/test_evolution_retry_idempotency.py
2026-07-17 11:43:42 +08:00

204 lines
6.9 KiB
Python

import asyncio
from datetime import datetime
from types import SimpleNamespace
from openspace.skill_engine.decision.analysis_adapter import AnalyzerDecisionAdapter
from openspace.skill_engine.evidence import EvidenceStore
from openspace.skill_engine.evidence.types import (
EvidencePacket,
EvidenceScope,
PacketBudget,
ResourceRef,
)
from openspace.skill_engine.evolution.engine import EvolutionEngine
from openspace.skill_engine.types import (
CaptureContract,
EvolutionSuggestion,
EvolutionType,
ExecutionAnalysis,
)
def _packet(packet_id: str, trigger_job_id: str = "trg_retry") -> EvidencePacket:
return EvidencePacket(
packet_id=packet_id,
trigger_job_id=trigger_job_id,
packet_type="analysis",
profile_name="analysis_current_task",
subprofile="task_finished",
manifest_watermark=1,
scope=EvidenceScope(task_id="task_retry", source_task_ids=("task_retry",)),
selected_refs={
"runtime_snapshot": [
ResourceRef(
ref_id="runtime:finish",
ref_type="runtime_snapshot",
task_id="task_retry",
preview="Task completed successfully.",
metadata={"status": "success"},
)
],
"tool_event": [
ResourceRef(
ref_id="tool:git",
ref_type="tool_event",
task_id="task_retry",
preview="Verified the reusable workflow.",
metadata={"tool_name": "bash", "status": "success"},
),
ResourceRef(
ref_id="tool:verify",
ref_type="tool_event",
task_id="task_retry",
preview="Independent verification passed.",
metadata={"tool_name": "bash", "status": "success"},
),
],
},
expanded_snippets=[],
readable_paths=[],
instructions={},
budget=PacketBudget(max_chars=1000, used_chars=100),
redaction_status="ok",
build_status="ok",
missing_ref_types=[],
)
def _analysis(suggestions: list[EvolutionSuggestion]) -> ExecutionAnalysis:
return ExecutionAnalysis(
task_id="task_retry",
timestamp=datetime.now(),
task_completed=True,
execution_note="Completed and verified.",
evolution_suggestions=suggestions,
)
def test_decision_identity_survives_packet_rebuild_and_suggestion_reordering() -> None:
first = EvolutionSuggestion(
evolution_type=EvolutionType.CAPTURED,
local_category_path="local/workflow/git-recovery",
direction="Capture the exact dangling-commit recovery workflow.",
capture_contract=_capture_contract("Recover a dangling commit."),
)
second = EvolutionSuggestion(
evolution_type=EvolutionType.CAPTURED,
local_category_path="local/tool-guide/read-before-write",
direction="Capture the exact read-before-write guard workflow.",
capture_contract=_capture_contract("Read a file before overwriting it."),
)
adapter = AnalyzerDecisionAdapter()
initial = adapter.from_analysis(
_analysis([first, second]),
_packet("pkt_initial"),
)
retried = adapter.from_analysis(
_analysis([second, first]),
_packet("pkt_retry"),
)
initial_ids = {item.reason_summary: item.decision_id for item in initial}
retried_ids = {item.reason_summary: item.decision_id for item in retried}
assert retried_ids == initial_ids
def _capture_contract(capability: str) -> CaptureContract:
return CaptureContract(
capability=capability,
procedure_refs=["tool:git"],
validation_refs=["tool:verify"],
validation_summary="A separate verification command passed.",
)
def test_evidence_store_loads_only_committed_action_for_decision(tmp_path) -> None:
store = EvidenceStore(tmp_path / "evidence.db")
try:
failed = store.begin_action(
action_id="act_failed",
decision_id="dec_retry",
trigger_job_id="trg_retry",
authoring_id="auth_failed",
validation_id="val_failed",
action_type="CAPTURED",
staging_dir="/tmp/stage-failed",
active_target_dir="/tmp/failed",
)
store.finalize_action(failed.action_id, status="failed")
assert store.load_committed_action_for_decision("dec_retry") is None
committed = store.begin_action(
action_id="act_committed",
decision_id="dec_retry",
trigger_job_id="trg_retry",
authoring_id="auth_committed",
validation_id="val_committed",
action_type="CAPTURED",
skill_id="git-recovery__v0_test",
staging_dir="/tmp/stage-committed",
active_target_dir="/tmp/committed",
)
store.finalize_action(
committed.action_id,
status="committed",
skill_id="git-recovery__v0_test",
)
loaded = store.load_committed_action_for_decision("dec_retry")
assert loaded is not None
assert loaded.action_id == "act_committed"
assert loaded.commit_status == "committed"
finally:
store.close()
def test_engine_reuses_committed_action_without_reauthoring() -> None:
decision = SimpleNamespace(
decision_id="dec_retry",
trigger_job_id="trg_retry",
proposed_action="CAPTURED",
admission={"outcome": "direct"},
)
existing_action = SimpleNamespace(
action_id="act_committed",
decision_id="dec_retry",
trigger_job_id="trg_retry",
commit_status="committed",
skill_id="git-recovery__v0_test",
)
class FakeEvidenceStore:
def load_committed_action_for_decision(self, decision_id: str):
assert decision_id == "dec_retry"
return existing_action
class ExplodingAuthoringBackend:
called = False
async def author_from_action_packet(self, *args, **kwargs):
self.called = True
raise AssertionError("committed decisions must not be re-authored")
authoring = ExplodingAuthoringBackend()
engine = EvolutionEngine(
authoring_backend=authoring,
committer=SimpleNamespace(
evidence_store=FakeEvidenceStore(),
skill_store=SimpleNamespace(load_record=lambda _skill_id: None),
),
evolution_mode="autonomous",
)
job = SimpleNamespace(
job_id="trg_retry",
status="running",
packet=SimpleNamespace(decisions=[decision]),
)
result = asyncio.run(engine.process_job(job))
assert result.status == "completed"
assert result.actions == [existing_action]
assert result.errors == []
assert authoring.called is False