"""Tests for the experiment-governance workflow state machine.""" from __future__ import annotations import pytest from spinor_os.config import AttributionMethod, Direction, EventType, LoopStage, MaturityStage from spinor_os.models import ( AttributionClaim, Event, Experiment, Hypothesis, Mission, PredictedEffect, ) from spinor_os.workflow import ( ExperimentGovernanceWorkflow, WorkflowContext, WorkflowTransitionError, ) def make_hypothesis() -> Hypothesis: return Hypothesis( statement="X increases Y", causal_claim="X causes Y to increase.", falsification_criteria=["Y does not increase"], predicted_effect=PredictedEffect( metric="y_rate", direction=Direction.INCREASE, magnitude=0.1, unit="pp", timing="7d", confidence=0.9, ), employee_owner="emp-001", ) def make_context(stage: LoopStage, **overrides) -> WorkflowContext: hypothesis = make_hypothesis() experiment = Experiment(hypothesis_id=hypothesis.hypothesis_id) experiment.set_stage(stage) return WorkflowContext( experiment=experiment, hypothesis=hypothesis, **overrides, ) def test_allowed_next_from_research(): wf = ExperimentGovernanceWorkflow() next_stages = wf.get_allowed_next(LoopStage.RESEARCH) assert LoopStage.ALLOCATE in next_stages assert LoopStage.DISASSEMBLE in next_stages def test_invalid_transition_raises(): wf = ExperimentGovernanceWorkflow() ctx = make_context(LoopStage.RESEARCH) with pytest.raises(WorkflowTransitionError): wf.advance(LoopStage.RESEARCH, LoopStage.SYSTEMIZE, ctx) def test_gates_require_mission_before_execute(): wf = ExperimentGovernanceWorkflow() ctx = make_context(LoopStage.ALLOCATE) ok, reason = wf.can_advance(LoopStage.ALLOCATE, LoopStage.EXECUTE, ctx) assert not ok assert "mission" in reason.lower() def test_gates_require_events_before_observe(): wf = ExperimentGovernanceWorkflow() ctx = make_context(LoopStage.EXECUTE) mission = Mission( experiment_id=ctx.experiment.experiment_id, hypothesis_id=ctx.hypothesis.hypothesis_id, employee_id="emp-001", modification="mod", customer_segment="a", territory="b", timing="c", ) ctx.mission = mission ok, _ = wf.can_advance(LoopStage.EXECUTE, LoopStage.OBSERVE, ctx) assert not ok def test_falsify_to_replicate_requires_significant_claim(): wf = ExperimentGovernanceWorkflow() hypothesis = make_hypothesis() experiment = Experiment(hypothesis_id=hypothesis.hypothesis_id) experiment.set_stage(LoopStage.ATTRIBUTE) claim = AttributionClaim( experiment_id=experiment.experiment_id, hypothesis_id=hypothesis.hypothesis_id, outcome_metric="y_rate", outcome_value=0.05, counterfactual_estimate=0.01, method=AttributionMethod.RCT, confidence=0.4, falsification_survived=True, ) ctx = WorkflowContext(experiment=experiment, hypothesis=hypothesis, claim=claim) ok, _ = wf.can_advance(LoopStage.ATTRIBUTE, LoopStage.FALSIFY, ctx) assert ok # Advance to falsify first. wf.advance(LoopStage.ATTRIBUTE, LoopStage.FALSIFY, ctx) # Then try to replicate with a non-significant claim. experiment.set_stage(LoopStage.FALSIFY) with pytest.raises(WorkflowTransitionError): wf.advance(LoopStage.FALSIFY, LoopStage.REPLICATE, ctx) def test_suggest_next_falsify_significant(): wf = ExperimentGovernanceWorkflow() hypothesis = make_hypothesis() experiment = Experiment(hypothesis_id=hypothesis.hypothesis_id) experiment.set_stage(LoopStage.FALSIFY) claim = AttributionClaim( experiment_id=experiment.experiment_id, hypothesis_id=hypothesis.hypothesis_id, outcome_metric="y_rate", outcome_value=0.2, counterfactual_estimate=0.05, method=AttributionMethod.RCT, confidence=0.95, falsification_survived=True, ) ctx = WorkflowContext(experiment=experiment, hypothesis=hypothesis, claim=claim) assert wf.suggest_next(ctx) == LoopStage.REPLICATE