| """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 |
|
|
| |
| wf.advance(LoopStage.ATTRIBUTE, LoopStage.FALSIFY, ctx) |
| |
| 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 |
|
|