| """Integration tests for real functionality: persistence, CSV import, and statistical attribution.""" |
|
|
| from __future__ import annotations |
|
|
| import os |
| import tempfile |
| from io import StringIO |
| from pathlib import Path |
|
|
| import pytest |
|
|
| from spinor_os import ExperimentationOS, PersistenceManager, import_events_csv |
| from spinor_os.config import AttributionMethod, EventType |
|
|
|
|
| def test_csv_import_allocates_missions_and_records_events(): |
| os = ExperimentationOS() |
| os.register_employee("import-001", "field-scientist", "northeast") |
| hypothesis = os.propose_hypothesis( |
| statement="Imported outreach increases appointments.", |
| causal_claim="Tailored outreach causes higher appointment rate.", |
| predicted_effect={ |
| "metric": "appointment_rate", |
| "direction": "increase", |
| "magnitude": 0.1, |
| "unit": "percentage_point", |
| "timing": "7d", |
| }, |
| employee_owner="import-001", |
| falsification_criteria=["No increase observed."], |
| customer_segment="enterprise", |
| territory="northeast", |
| modification="imported_outreach", |
| ) |
| experiment = os.start_experiment(hypothesis.hypothesis_id) |
|
|
| csv_text = f"""experiment_id,actor_id,event_type,outcome_value,metric,execution_quality,actor_role |
| {experiment.experiment_id},import-001,OUTCOME_OBSERVED,0.15,appointment_rate,0.88,field-scientist |
| {experiment.experiment_id},import-001,OUTCOME_OBSERVED,0.18,appointment_rate,0.90,field-scientist |
| """ |
| summary = import_events_csv(os, StringIO(csv_text), experiment_id=experiment.experiment_id) |
| assert summary["rows_read"] == 2 |
| assert summary["rows_ingested"] == 2 |
| assert len(summary["event_ids"]) == 2 |
| assert not summary["errors"] |
| assert len(os.events) == 2 |
|
|
|
|
| def test_real_attribution_ols_and_diff_in_diff(): |
| os = ExperimentationOS() |
| os.register_employee("alice", "field-scientist", "northeast") |
| os.register_employee("bob", "field-scientist", "southwest") |
|
|
| hypothesis = os.propose_hypothesis( |
| statement="X lifts Y in enterprise segment", |
| causal_claim="X causes Y", |
| predicted_effect={ |
| "metric": "y_rate", |
| "direction": "increase", |
| "magnitude": 0.1, |
| "unit": "pp", |
| "timing": "7d", |
| }, |
| employee_owner="alice", |
| falsification_criteria=["no effect"], |
| customer_segment="enterprise", |
| territory="northeast", |
| modification="treatment_a", |
| ) |
|
|
| treat = os.start_experiment(hypothesis.hypothesis_id) |
| control = os.start_experiment(hypothesis.hypothesis_id) |
|
|
| m_t = os.allocate_mission( |
| treat.experiment_id, |
| "alice", |
| [ |
| { |
| "hypothesis_id": hypothesis.hypothesis_id, |
| "modification": "treatment_a", |
| "customer_segment": "enterprise", |
| "territory": "northeast", |
| "timing": "2026-W31", |
| "resource_allocation": 1.0, |
| } |
| ], |
| ) |
| m_c = os.allocate_mission( |
| control.experiment_id, |
| "bob", |
| [ |
| { |
| "hypothesis_id": hypothesis.hypothesis_id, |
| "modification": "control", |
| "customer_segment": "enterprise", |
| "territory": "southwest", |
| "timing": "2026-W31", |
| "resource_allocation": 1.0, |
| } |
| ], |
| ) |
|
|
| os.record_event( |
| treat.experiment_id, |
| m_t.mission_id, |
| EventType.OUTCOME_OBSERVED, |
| "alice", |
| outcome_value=0.05, |
| metric="y_rate", |
| execution_quality=0.9, |
| ) |
| os.record_event( |
| control.experiment_id, |
| m_c.mission_id, |
| EventType.OUTCOME_OBSERVED, |
| "bob", |
| outcome_value=0.07, |
| metric="y_rate", |
| execution_quality=0.9, |
| ) |
| os.record_event( |
| treat.experiment_id, |
| m_t.mission_id, |
| EventType.OUTCOME_OBSERVED, |
| "alice", |
| outcome_value=0.35, |
| metric="y_rate", |
| execution_quality=0.9, |
| ) |
| os.record_event( |
| control.experiment_id, |
| m_c.mission_id, |
| EventType.OUTCOME_OBSERVED, |
| "bob", |
| outcome_value=0.07, |
| metric="y_rate", |
| execution_quality=0.85, |
| ) |
|
|
| claim_ols = os.attribute( |
| treat.experiment_id, |
| "y_rate", |
| 0.35, |
| 0.05, |
| AttributionMethod.OLS, |
| 0.95, |
| falsification_survived=True, |
| ) |
| assert claim_ols.real_attribution is not None |
| assert claim_ols.real_attribution["method"] == "ols_regression" |
| assert claim_ols.real_attribution["estimated_effect"] > 0 |
| assert claim_ols.real_attribution["n_observations"] >= 4 |
|
|
| claim_did = os.attribute( |
| treat.experiment_id, |
| "y_rate", |
| 0.35, |
| 0.05, |
| AttributionMethod.DIFF_IN_DIFF, |
| 0.95, |
| falsification_survived=True, |
| ) |
| assert claim_did.real_attribution is not None |
| assert claim_did.real_attribution["method"] == "difference_in_differences" |
| assert claim_did.real_attribution["estimated_effect"] > 0 |
| assert claim_did.real_attribution["n_observations"] >= 4 |
|
|
|
|
| def test_persistence_reloads_imported_data(): |
| with tempfile.TemporaryDirectory() as tmp: |
| db = Path(tmp) / "spinor_real.sqlite" |
| os1 = ExperimentationOS(persistence=PersistenceManager(db)) |
| os1.register_employee("persist-001", "field-scientist", "northeast") |
| hypothesis = os1.propose_hypothesis( |
| statement="Persistent outreach works.", |
| causal_claim="Outreach causes lift.", |
| predicted_effect={ |
| "metric": "lift", |
| "direction": "increase", |
| "magnitude": 0.1, |
| "unit": "pp", |
| "timing": "7d", |
| }, |
| employee_owner="persist-001", |
| falsification_criteria=["no lift"], |
| customer_segment="enterprise", |
| territory="northeast", |
| modification="outreach", |
| ) |
| experiment = os1.start_experiment(hypothesis.hypothesis_id) |
|
|
| csv_text = f"""experiment_id,actor_id,event_type,outcome_value,metric,execution_quality |
| {experiment.experiment_id},persist-001,OUTCOME_OBSERVED,0.12,lift,0.88 |
| """ |
| summary = import_events_csv(os1, StringIO(csv_text), experiment_id=experiment.experiment_id) |
| assert summary["rows_ingested"] == 1 |
|
|
| |
| os2 = ExperimentationOS(persistence=PersistenceManager(db)) |
| assert "persist-001" in os2.employees |
| assert experiment.experiment_id in os2.experiments |
| assert len(os2.events) == 1 |
|
|
|
|
| def test_server_import_and_real_attribution(client): |
| """Live-server style verification using the FastAPI TestClient.""" |
| |
| r = client.post("/employees", json={ |
| "employee_id": "real-api-001", |
| "role": "field-scientist", |
| "territory": "northeast", |
| }) |
| assert r.status_code == 200 |
|
|
| r = client.post("/hypotheses", json={ |
| "statement": "Real outreach increases conversions", |
| "causal_claim": "Outreach causes conversions", |
| "predicted_effect": { |
| "metric": "real_conversion_rate", |
| "direction": "increase", |
| "magnitude": 0.1, |
| "unit": "pp", |
| "timing": "7d", |
| }, |
| "employee_owner": "real-api-001", |
| "falsification_criteria": ["No lift"], |
| "customer_segment": "enterprise", |
| "territory": "northeast", |
| "modification": "real_outreach", |
| }) |
| assert r.status_code == 200 |
| hypothesis_id = r.json()["hypothesis_id"] |
|
|
| |
| treat = client.post("/experiments", json={"hypothesis_id": hypothesis_id}).json() |
| control = client.post("/experiments", json={"hypothesis_id": hypothesis_id}).json() |
| treat_id = treat["experiment_id"] |
| control_id = control["experiment_id"] |
|
|
| |
| m_t = client.post(f"/experiments/{treat_id}/missions", json={ |
| "employee_id": "real-api-001", |
| "candidates": [{ |
| "hypothesis_id": hypothesis_id, |
| "modification": "real_outreach", |
| "customer_segment": "enterprise", |
| "territory": "northeast", |
| "timing": "2026-W31", |
| "resource_allocation": 1.0, |
| }], |
| }).json() |
| m_c = client.post(f"/experiments/{control_id}/missions", json={ |
| "employee_id": "real-api-001", |
| "candidates": [{ |
| "hypothesis_id": hypothesis_id, |
| "modification": "control", |
| "customer_segment": "enterprise", |
| "territory": "northeast", |
| "timing": "2026-W31", |
| "resource_allocation": 1.0, |
| }], |
| }).json() |
|
|
| |
| for (exp, mid, val, eq) in [ |
| (treat_id, m_t["mission_id"], 0.05, 0.9), |
| (control_id, m_c["mission_id"], 0.06, 0.9), |
| (treat_id, m_t["mission_id"], 0.35, 0.9), |
| (control_id, m_c["mission_id"], 0.07, 0.85), |
| ]: |
| r = client.post(f"/experiments/{exp}/events", json={ |
| "mission_id": mid, |
| "event_type": "outcome_observed", |
| "actor_id": "real-api-001", |
| "outcome_value": val, |
| "metric": "real_conversion_rate", |
| "execution_quality": eq, |
| }) |
| assert r.status_code == 200 |
|
|
| |
| r = client.post(f"/experiments/{treat_id}/attributes", json={ |
| "outcome_metric": "real_conversion_rate", |
| "outcome_value": 0.35, |
| "counterfactual_estimate": 0.05, |
| "method": "ols", |
| "confidence": 0.95, |
| "falsification_survived": True, |
| }) |
| assert r.status_code == 200 |
| data = r.json() |
| assert data["real_attribution"] is not None |
| assert data["real_attribution"]["method"] == "ols_regression" |
| assert data["real_attribution"]["estimated_effect"] > 0 |
|
|
| |
| r = client.post("/persistence/save") |
| assert r.status_code == 200 |
| r = client.get("/persistence/status") |
| assert r.status_code == 200 |
| status = r.json() |
| assert status["persistence_enabled"] is True |
| assert status["experiments"] >= 2 |
|
|