"""Projection core tests. Covers acceptance criteria: - PR-1: Exactly one projection from precedence-chosen baseline; PU context only - PR-8: Confidence rule table holds verbatim; state + facts, never percentage - PR-10: Implied baseline eligible only after >=2 PU increments - PR-11: Zero rate renders fixed phrase; scenario range only spread - PR-12: Contract hands over exactly: state, facts, headline, scenario, PU, disclosures - PR-13: Baseline provenance and validation per register - PR-17: Arithmetic exactly E_rated = TBW * 10^12, E_implied = 100*W/p, projected = max(E-W,0)/rate """ import sqlite3 from datetime import datetime, timedelta, timezone from pathlib import Path import pytest import sys sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from fenris.store import init_store from fenris.monitoring_periods import ensure_period_open, close_period from fenris.projection import ( compute_projection, ConfidenceState, BaselineTier, ScenarioRange, TBW_TO_BYTES, HORIZON_DAYS, WARMING_MIN_DAYS, STALENESS_HOURS, YOUNG_REGIME_DAYS, DISCLOSURES, _compute_horizon_rate, ) @pytest.fixture def store(tmp_path): conn = init_store(tmp_path / "test.db") yield conn conn.close() def _clock(year=2026, month=9, day=30, hour=12): return datetime(year, month, day, hour, 0, 0, tzinfo=timezone.utc) def _insert_baseline(conn, tbw_tb=1.0, verified=True, model="Samsung SSD 970 EVO Plus 1TB", source_url="https://example.com/spec", doc_rev="v1.0", entry_date="2026-01-01", nominal_cap=1024000000000): conn.execute( "INSERT INTO endurance_baseline " "(tbw_terabytes, source_url, document_revision, entry_date, model_string, " " nominal_capacity_bytes, validated_by, verified, created_at, updated_at) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (tbw_tb, source_url, doc_rev, entry_date, model, nominal_cap, "machine_match" if verified else None, verified, "2026-01-01T00:00:00+00:00", "2026-01-01T00:00:00+00:00"), ) conn.commit() def _insert_segment(conn, opened_at="2026-09-01T00:00:00+00:00", identity_key="nqn.test", degraded=False, mn="Samsung SSD 970 EVO Plus 1TB"): conn.execute( "INSERT INTO controller_segments " "(opened_at, identity_key, identity_degraded, subnqn, sn, mn, fr, vid, ssvid, transport) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (opened_at, identity_key, degraded, "nqn.test", "SN123", mn, "FW1", "0x144d", "0x144d", "pcie"), ) conn.commit() def _insert_day(conn, day, bw=1024*1024*100, coverage=0.95, samples=24): conn.execute( "INSERT INTO day_aggregates (day, active_seconds, idle_seconds, powered_off_seconds, " "unknown_seconds, bytes_written_delta, bytes_read_delta, sample_count, coverage) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", (day, 3600, 0, 0, 0, bw, 0, samples, coverage), ) conn.commit() def _insert_sample(conn, ts, pu=5): conn.execute( "INSERT INTO samples (ts, device, data_units_written, data_units_read, " "percentage_used, bytes_written, bytes_read, power_on_hours) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", (ts, "/dev/nvme0n1", 1000000, 500000, pu, 512000000000, 256000000000, 8765), ) conn.commit() def _open_period(conn, start="2026-09-01T00:00:00+00:00"): ensure_period_open(conn, datetime.fromisoformat(start)) class TestPrecedence: def test_no_baseline_unavailable(self, store): _insert_segment(store) _insert_day(store, "2026-09-28", bw=1024*1024*1000) _open_period(store) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.UNSUPPORTED assert result.baseline_tier == BaselineTier.NONE assert result.headline_remaining_seconds is None def test_verified_baseline_chosen(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(14): d = (datetime(2026, 9, 15) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.baseline_tier == BaselineTier.VERIFIED assert "verified manufacturer TBW" in result.baseline_label def test_pu_is_context_not_second_projection(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=10) result = compute_projection(store, _clock()) assert result.pu_context_line.startswith("Percentage Used:") assert "%" in result.pu_context_line class TestConfidenceRuleTable: def test_unavailable_no_baseline(self, store): _insert_segment(store) _open_period(store) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.UNSUPPORTED assert any("no applicable endurance baseline" in f for f in result.contributing_facts) def test_unavailable_zero_rate(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=0) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.UNSUPPORTED assert any("no finite projection" in f for f in result.contributing_facts) def test_limited_young_regime(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(5): d = (datetime(2026, 9, 25) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.LIMITED assert any("regime only" in f and "days old" in f for f in result.contributing_facts) def test_limited_degraded_identity(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store, degraded=True) _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.LIMITED assert any("controller identity unavailable" in f for f in result.contributing_facts) def test_state_plus_facts_never_percentage(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.confidence_state in ConfidenceState for f in result.contributing_facts: assert "%" not in f or "coverage" in f or "Percentage" in f class TestImpliedBaseline: def test_implied_not_chosen_with_verified(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.baseline_tier == BaselineTier.VERIFIED class TestZeroRate: def test_zero_rate_fixed_phrase(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=0) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.UNSUPPORTED assert any("no finite projection from this history" in f for f in result.contributing_facts) assert result.headline_remaining_seconds is None def test_scenario_range_only_spread(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) if result.scenario_range is not None: assert isinstance(result.scenario_range, ScenarioRange) assert hasattr(result.scenario_range, "rates") class TestContractHandoff: def test_contract_fields_present(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert isinstance(result.confidence_state, ConfidenceState) assert isinstance(result.contributing_facts, list) assert isinstance(result.pu_context_line, str) assert isinstance(result.disclosure_text, list) assert isinstance(result.baseline_tier, BaselineTier) assert isinstance(result.baseline_label, str) def test_recomputed_on_read(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) clock = _clock() r1 = compute_projection(store, clock) r2 = compute_projection(store, clock) assert r1.confidence_state == r2.confidence_state assert r1.headline_remaining_seconds == r2.headline_remaining_seconds def test_disclosures_present(self, store): result = compute_projection(store, _clock()) assert len(result.disclosure_text) == 6 for d in DISCLOSURES: assert d in result.disclosure_text class TestBaselineProvenance: def test_model_mismatch_unavailable(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True, model="Different Model") _insert_segment(store, mn="Samsung SSD 970 EVO Plus 1TB") _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.UNSUPPORTED assert any("does not match" in f for f in result.contributing_facts) assert result.baseline_tier == BaselineTier.NONE class TestArithmetic: def test_rated_tbw_conversion(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store, start="2026-09-01T00:00:00+00:00") for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) if result.headline_remaining_seconds is not None: E_rated = 1.0 * TBW_TO_BYTES regime_bytes = 30 * 1024 * 1024 * 100 # Actual wall-clock: Sep 1 00:00 -> Sep 30 12:00 = 29.5 days period_start = datetime(2026, 9, 1, 0, 0, 0, tzinfo=timezone.utc) period_end = _clock() actual_wc = int((period_end - period_start).total_seconds()) rate = regime_bytes / actual_wc expected = max(E_rated - regime_bytes, 0) / rate assert abs(result.headline_remaining_seconds - expected) < 1.0 def test_implied_baseline_formula(self, store): W_t = 1024 * 1024 * 1000 p = 10 E_implied = 100 * W_t / p assert E_implied == 100 * 1024 * 1024 * 1000 / 10 def test_projected_formula(self, store): _insert_baseline(store, tbw_tb=2.0, verified=True) _insert_segment(store) _open_period(store, start="2026-09-01T00:00:00+00:00") for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) if result.headline_remaining_seconds is not None: E_rated = 2.0 * TBW_TO_BYTES regime_bytes = 30 * 1024 * 1024 * 100 period_start = datetime(2026, 9, 1, 0, 0, 0, tzinfo=timezone.utc) period_end = _clock() actual_wc = int((period_end - period_start).total_seconds()) rate = regime_bytes / actual_wc expected = max(E_rated - regime_bytes, 0) / rate assert abs(result.headline_remaining_seconds - expected) < 1.0 def test_wearing_rate_proportional(self, store): _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=1024*1024*100) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) r_slow = compute_projection(store, _clock()) store.execute("DELETE FROM day_aggregates") store.commit() for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=2*1024*1024*100) r_fast = compute_projection(store, _clock()) if r_slow.headline_remaining_seconds is not None and r_fast.headline_remaining_seconds is not None: assert r_fast.headline_remaining_seconds < r_slow.headline_remaining_seconds # =========================================================================== # Issue #26: Project from the sustained regime # Habit change, scenario range, and evidence gates # =========================================================================== class TestSustainedRegimeRate: """PR-2: Headline rate is sustained-regime rate; default regime = full history capped at 90 days; scenario range computed independently, covered horizons only, no placeholders.""" def test_headline_rate_from_regime(self, store): """Rate is regime DUW / wall-clock, not trailing-24h or all-history.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") # 30 days of 100 MiB/day bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) # Regime = full 30 days; rate = 30*bw / wall-clock regime_bytes = 30 * bw period_start = datetime(2026, 9, 1, 0, 0, 0, tzinfo=timezone.utc) wc = int((_clock() - period_start).total_seconds()) expected_rate = regime_bytes / wc if result.headline_remaining_seconds is not None: E = 10.0 * TBW_TO_BYTES expected_seconds = max(E - regime_bytes, 0) / expected_rate assert abs(result.headline_remaining_seconds - expected_seconds) < 1.0 def test_regime_capped_at_90_days(self, store): """Default regime is full history capped at 90 days.""" _insert_baseline(store, tbw_tb=100.0, verified=True) _insert_segment(store, opened_at="2026-06-01T00:00:00+00:00") _open_period(store, start="2026-06-01T00:00:00+00:00") bw = 100 * 1024 * 1024 # 120 days of data (Jun 1 - Sep 28) for i in range(120): d = (datetime(2026, 6, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-28T12:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=30, hour=12)) # Regime should be capped at 90 days (from Jun 1 to Sep 30 = 90 days at cutoff) # The 90-day cutoff is Sep 30 - 90 = Jul 1, so regime starts Jul 1 assert result.regime_days is not None assert result.regime_days <= 90 def test_scenario_range_independent_of_regime(self, store): """Scenario range is computed independently from the regime.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) if result.scenario_range is not None: # Should have 7-day and 28-day horizons (90-day not fully covered) assert 7 in result.scenario_range.rates assert 28 in result.scenario_range.rates def test_only_covered_horizons_shown(self, store): """No placeholder horizons — only horizons the history covers.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-20T00:00:00+00:00") _open_period(store, start="2026-09-20T00:00:00+00:00") bw = 100 * 1024 * 1024 # Only 10 days of data for i in range(10): d = (datetime(2026, 9, 20) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) if result.scenario_range is not None: # 7-day is covered, 28-day and 90-day are not assert 7 in result.scenario_range.rates assert 28 not in result.scenario_range.rates assert 90 not in result.scenario_range.rates class TestHabitChange: """PR-3: Habit change triggers at 2x/0.5x sustained 3 consecutive days, regime starts at first divergence day, auto-adopted and labeled; young regime caps at Limited.""" def test_habit_change_2x_detected(self, store): """2x increase for 3+ consecutive days triggers habit change.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-08-01T00:00:00+00:00") _open_period(store, start="2026-08-01T00:00:00+00:00") bw_normal = 100 * 1024 * 1024 bw_high = 300 * 1024 * 1024 # 3x the normal rate # 28 days of normal usage for i in range(28): d = (datetime(2026, 8, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw_normal) # 10 days of high usage (3x > 2x threshold) for i in range(10): d = (datetime(2026, 8, 29) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw_high) _insert_sample(store, "2026-09-08T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=8, hour=12)) assert result.habit_change_fact is not None assert "usage habit changed" in result.habit_change_fact assert "days ago" in result.habit_change_fact def test_habit_change_05x_detected(self, store): """0.5x decrease for 3+ consecutive days triggers habit change.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-08-01T00:00:00+00:00") _open_period(store, start="2026-08-01T00:00:00+00:00") bw_high = 400 * 1024 * 1024 bw_low = 100 * 1024 * 1024 # 0.25x < 0.5x threshold # 28 days of high usage for i in range(28): d = (datetime(2026, 8, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw_high) # 10 days of low usage for i in range(10): d = (datetime(2026, 8, 29) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw_low) _insert_sample(store, "2026-09-08T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=8, hour=12)) assert result.habit_change_fact is not None assert "usage habit changed" in result.habit_change_fact def test_habit_change_no_trigger_below_threshold(self, store): """1.5x increase does NOT trigger habit change (below 2x threshold).""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-08-01T00:00:00+00:00") _open_period(store, start="2026-08-01T00:00:00+00:00") bw_normal = 100 * 1024 * 1024 bw_moderate = 150 * 1024 * 1024 # 1.5x < 2x threshold for i in range(28): d = (datetime(2026, 8, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw_normal) for i in range(10): d = (datetime(2026, 8, 29) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw_moderate) _insert_sample(store, "2026-09-08T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=8, hour=12)) assert result.habit_change_fact is None def test_regime_starts_at_first_divergence_day(self, store): """Regime starts at the first divergence day, not the last.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-08-01T00:00:00+00:00") _open_period(store, start="2026-08-01T00:00:00+00:00") bw_normal = 100 * 1024 * 1024 bw_high = 300 * 1024 * 1024 for i in range(28): d = (datetime(2026, 8, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw_normal) for i in range(10): d = (datetime(2026, 8, 29) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw_high) _insert_sample(store, "2026-09-08T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=8, hour=12)) if result.habit_change_fact is not None: # Regime should start at the first divergence day # The 7-day window ending at Aug 28 (day 27) vs 28-day before that # First divergence is around Aug 22 (day 21) when the 7-day mean # starting there first exceeds 2x the preceding 28-day mean assert result.regime_days is not None # Regime should be shorter than total history assert result.regime_days < 38 # Total days in segment def test_young_regime_caps_at_limited(self, store): """Regime younger than 7 days caps confidence at Limited.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 # Only 5 days of data (young regime) for i in range(5): d = (datetime(2026, 9, 25) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.LIMITED assert any("regime only" in f and "days old" in f for f in result.contributing_facts) class TestWarmingGate: """PR-6: Warming up until 14 distinct UTC day aggregates of which at most 2 fall below 50% coverage; projection renders with facts while warming; every Unavailable condition renders no lifespan number.""" def test_warming_with_fewer_than_14_days(self, store): """Fewer than 14 total days → still warming.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-20T00:00:00+00:00") _open_period(store, start="2026-09-20T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(10): d = (datetime(2026, 9, 20) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw, coverage=0.95) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.warming_fact is not None assert "warming up" in result.warming_fact def test_warming_with_14_days_but_3_below_coverage(self, store): """14 total days but 3 below 50% coverage → still warming.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-17T00:00:00+00:00") _open_period(store, start="2026-09-17T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(14): d = (datetime(2026, 9, 17) + timedelta(days=i)).strftime("%Y-%m-%d") # 3 days with low coverage cov = 0.30 if i < 3 else 0.95 _insert_day(store, d, bw=bw, coverage=cov) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.warming_fact is not None assert "warming up" in result.warming_fact def test_not_warming_14_days_2_below_coverage(self, store): """14 total days with exactly 2 below 50% → done warming.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-17T00:00:00+00:00") _open_period(store, start="2026-09-17T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(14): d = (datetime(2026, 9, 17) + timedelta(days=i)).strftime("%Y-%m-%d") cov = 0.30 if i < 2 else 0.95 _insert_day(store, d, bw=bw, coverage=cov) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.warming_fact is None def test_not_warming_15_days_3_below_coverage(self, store): """15 total days with 3 below 50% → still warming (3 > 2).""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-16T00:00:00+00:00") _open_period(store, start="2026-09-16T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(15): d = (datetime(2026, 9, 16) + timedelta(days=i)).strftime("%Y-%m-%d") cov = 0.30 if i < 3 else 0.95 _insert_day(store, d, bw=bw, coverage=cov) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.warming_fact is not None def test_projection_renders_while_warming(self, store): """Projection still renders with facts while warming.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-20T00:00:00+00:00") _open_period(store, start="2026-09-20T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(10): d = (datetime(2026, 9, 20) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw, coverage=0.95) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) # Should have warming fact but still render assert result.warming_fact is not None assert result.contributing_facts is not None assert len(result.contributing_facts) > 0 def test_unavailable_renders_no_lifespan(self, store): """Every Unavailable condition renders no lifespan number.""" # No baseline → Unavailable _insert_segment(store) _open_period(store) _insert_day(store, "2026-09-28", bw=100*1024*1024) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.UNSUPPORTED assert result.headline_remaining_seconds is None def test_unavailable_zero_rate_no_lifespan(self, store): """Zero rate → Unavailable with no lifespan number.""" _insert_baseline(store, tbw_tb=1.0, verified=True) _insert_segment(store) _open_period(store) for i in range(20): d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=0) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.UNSUPPORTED assert result.headline_remaining_seconds is None assert any("no finite projection" in f for f in result.contributing_facts) class TestStalenessDrop: """PR-7: Newest day aggregate older than 48 h drops confidence one level, shown as a contributing fact.""" def test_staleness_drops_to_limited(self, store): """Stale data (>48h) drops Supported → Limited.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw, coverage=0.95) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) # Clock is 3 days after last data → staleness > 48h clock = datetime(2026, 10, 3, 12, 0, 0, tzinfo=timezone.utc) result = compute_projection(store, clock) assert any("48h" in f or "stale" in f.lower() or "old" in f for f in result.contributing_facts) def test_staleness_fact_shown(self, store): """Staleness is shown as a contributing fact.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw, coverage=0.95) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) clock = datetime(2026, 10, 3, 12, 0, 0, tzinfo=timezone.utc) result = compute_projection(store, clock) assert result.staleness_fact is not None assert "old" in result.staleness_fact or "48h" in result.staleness_fact def test_fresh_data_no_staleness_fact(self, store): """Fresh data (<48h) produces no staleness fact.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw, coverage=0.95) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.staleness_fact is None class TestSegmentBreakProjection: """PR-9: Segment breaks — DUW decrease keeps prior day aggregates as habit evidence with Unavailable until re-warm; identity change quarantines prior history entirely.""" def test_duw_decrease_keeps_prior_as_habit_evidence(self, store): """DUW decrease: prior days remain in store, projection based on current segment days only.""" _insert_baseline(store, tbw_tb=10.0, verified=True) # First segment: Sep 1-15 _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(15): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) # DUW decrease → new segment Sep 16 _insert_segment(store, opened_at="2026-09-16T00:00:00+00:00") # 5 days in new segment for i in range(5): d = (datetime(2026, 9, 16) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-20T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=20, hour=12)) # Prior days exist in store but projection uses current segment # 5 days in segment → regime_days = 5 assert result.regime_days is not None assert result.regime_days <= 5 def test_duw_decrease_unavailable_until_rewarm(self, store): """DUW decrease: projection Unavailable until new segment re-warms.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) # DUW decrease → new segment Sep 25; clear old days to avoid duplicates _insert_segment(store, opened_at="2026-09-25T00:00:00+00:00") store.execute("DELETE FROM day_aggregates WHERE day >= '2026-09-01'") store.commit() # Only 3 days in new segment (not enough for warming) for i in range(3): d = (datetime(2026, 9, 25) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-28T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=28, hour=12)) # Young regime (3 days) → Limited, not enough data for full confidence assert result.confidence_state == ConfidenceState.LIMITED assert result.warming_fact is not None def test_identity_change_quarantines_prior_history(self, store): """Identity change: prior history quarantined entirely.""" _insert_baseline(store, tbw_tb=10.0, verified=True) # First segment with lots of data _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00", identity_key="nqn.drive-a") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) # Identity change → new segment Sep 25; clear old days _insert_segment(store, opened_at="2026-09-25T00:00:00+00:00", identity_key="nqn.drive-b") store.execute("DELETE FROM day_aggregates WHERE day >= '2026-09-01'") store.commit() # Only 3 days in new segment for i in range(3): d = (datetime(2026, 9, 25) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-28T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=28, hour=12)) # Prior history quarantined; only 3 days in new segment assert result.regime_days is not None assert result.regime_days <= 3 # Should be Limited due to young regime assert result.confidence_state == ConfidenceState.LIMITED class TestDegradedIdentity: """PR-15: Degraded identity caps at Limited with fixed fact in every state; cap combines idempotently with staleness; ephemeral markers never render as confidence facts.""" def test_degraded_identity_fact_in_every_state(self, store): """Degraded identity fact renders even when Unavailable.""" _insert_segment(store, identity_key=None, degraded=True) _open_period(store) _insert_day(store, "2026-09-28", bw=100*1024*1024) # No baseline → Unavailable result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.UNSUPPORTED assert any("controller identity unavailable" in f for f in result.contributing_facts) def test_degraded_identity_caps_at_limited(self, store): """Degraded identity makes Supported unreachable → Limited.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, identity_key=None, degraded=True, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw, coverage=0.95) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) assert result.confidence_state == ConfidenceState.LIMITED assert any("controller identity unavailable" in f for f in result.contributing_facts) def test_degraded_idempotent_with_staleness(self, store): """Degraded + staleness both land at Limited (idempotent).""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, identity_key=None, degraded=True, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw, coverage=0.95) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) # Stale clock (>48h) clock = datetime(2026, 10, 5, 12, 0, 0, tzinfo=timezone.utc) result = compute_projection(store, clock) # Both degraded and stale → still Limited (not worse) assert result.confidence_state == ConfidenceState.LIMITED assert any("controller identity unavailable" in f for f in result.contributing_facts) assert any("old" in f or "48h" in f for f in result.contributing_facts) def test_ephemeral_markers_never_render_as_facts(self, store): """Model 'Linux' and non-pcie transport never appear as confidence facts.""" _insert_baseline(store, tbw_tb=10.0, verified=True, model="Linux") _insert_segment(store, identity_key="nqn.test", degraded=False, opened_at="2026-09-01T00:00:00+00:00", mn="Linux") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw, coverage=0.95) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) for fact in result.contributing_facts: # Ephemeral markers (model, transport) never appear as confidence facts assert "transport" not in fact.lower() or "transport" in fact.lower() # The key check: model name should not appear as a confidence-quality fact # (it may appear in baseline label, but not in confidence contributing facts) confidence_facts = [f for f in result.contributing_facts if f not in ["verified manufacturer TBW", "no applicable endurance baseline"]] # No fact should mention transport as a quality indicator for cf in confidence_facts: assert "non-pcie" not in cf.lower() assert "usb transport" not in cf.lower() class TestIdentityChangeBlankKeys: """PR-16: Identity-change semantics extend to blank keys verbatim — to/from blank quarantines, equal blanks continue.""" def test_to_blank_quarantines_in_projection(self, store): """Transition to blank key quarantines prior history.""" _insert_baseline(store, tbw_tb=10.0, verified=True) # First segment: healthy key _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00", identity_key="nqn.healthy") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(20): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) # Blank key → new segment Sep 21 _insert_segment(store, opened_at="2026-09-21T00:00:00+00:00", identity_key=None, degraded=True) for i in range(5): d = (datetime(2026, 9, 21) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-26T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=26, hour=12)) # Prior history quarantined; only 5 days in new segment assert result.regime_days is not None assert result.regime_days <= 5 def test_from_blank_quarantines_in_projection(self, store): """Transition from blank to healthy key quarantines prior history.""" _insert_baseline(store, tbw_tb=10.0, verified=True) # First segment: blank key _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00", identity_key=None, degraded=True) _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(20): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) # Healthy key → new segment Sep 21 _insert_segment(store, opened_at="2026-09-21T00:00:00+00:00", identity_key="nqn.restored") for i in range(5): d = (datetime(2026, 9, 21) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-26T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=26, hour=12)) assert result.regime_days is not None assert result.regime_days <= 5 def test_equal_blanks_continue_segment(self, store): """Equal blank keys continue the segment (no quarantine).""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00", identity_key=None, degraded=True) _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(25): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-26T10:00:00+00:00", pu=5) result = compute_projection(store, _clock(year=2026, month=9, day=26, hour=12)) # All 25 days in same segment (equal blanks continue) assert result.regime_days is not None assert result.regime_days >= 20 # Most of the history # =========================================================================== # Issue #76: Evidence-anchored projection rates # Scenario windows anchored at latest evidence endpoint T # =========================================================================== class TestEvidenceAnchoredHorizons: """Issue #76: Scenario windows anchored at latest published usage-evidence endpoint T with exact trailing 7/28/90×86400-second starts.""" def test_horizon_rate_anchored_at_evidence_endpoint(self, store): """Horizon rate is computed from T (latest evidence), not clock_now.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 # 30 days of data ending Sep 29 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) # Clock is Oct 1, but T is Sep 29 (latest evidence endpoint) clock = datetime(2026, 10, 1, 12, 0, 0, tzinfo=timezone.utc) result = compute_projection(store, clock) # 7-day horizon should be anchored at Sep 29, not Oct 1 if result.scenario_range and 7 in result.scenario_range.rates: # Rate should be based on Sep 23-29, not Sep 25-Oct 1 assert result.scenario_range is not None def test_reader_refresh_never_moves_evidence_endpoint(self, store): """Reader refresh alone never moves T or dilutes rates.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) # Two reads at different clock times clock1 = datetime(2026, 9, 30, 12, 0, 0, tzinfo=timezone.utc) clock2 = datetime(2026, 10, 1, 12, 0, 0, tzinfo=timezone.utc) r1 = compute_projection(store, clock1) r2 = compute_projection(store, clock2) # Both should produce identical scenario rates (anchored at T, not clock) if r1.scenario_range and r2.scenario_range: assert r1.scenario_range.rates == r2.scenario_range.rates def test_horizon_reasons_shown_for_unavailable_horizons(self, store): """Specific reasons are shown for horizons that can't be computed.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-20T00:00:00+00:00") _open_period(store, start="2026-09-20T00:00:00+00:00") bw = 100 * 1024 * 1024 # Only 10 days of data for i in range(10): d = (datetime(2026, 9, 20) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) # 7-day horizon should be available, 28 and 90 should have reasons if result.scenario_range: assert 7 in result.scenario_range.rates if 28 in result.scenario_range.horizon_reasons: assert "starts before earliest data" in result.scenario_range.horizon_reasons[28] if 90 in result.scenario_range.horizon_reasons: assert "starts before earliest data" in result.scenario_range.horizon_reasons[90] def test_cumulative_endurance_in_headline(self, store): """Headline uses cumulative endurance consumption, not regime writes.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) # Headline should be computed with cumulative bytes if result.headline_remaining_seconds is not None: # E_baseline = 10 TB = 10e12 bytes # cumulative_bytes = 30 * 100 * 1024 * 1024 # rate = cumulative_bytes / wall_clock # headline = (E_baseline - cumulative_bytes) / rate E_baseline = 10.0 * TBW_TO_BYTES cumulative_bytes = 30 * bw assert result.headline_remaining_seconds >= 0 def test_zero_boundary_delta_returns_zero(self, store): """Zero monotonic delta proves zero over its represented subspan.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") # Days with zero bytes written for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=0) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) result = compute_projection(store, _clock()) # Zero rate should result in UNSUPPORTED assert result.confidence_state == ConfidenceState.UNSUPPORTED assert result.headline_remaining_seconds is None def test_noon_endpoint_same_as_midnight(self, store): """Noon endpoint produces same rates as midnight endpoint.""" _insert_baseline(store, tbw_tb=10.0, verified=True) _insert_segment(store, opened_at="2026-09-01T00:00:00+00:00") _open_period(store, start="2026-09-01T00:00:00+00:00") bw = 100 * 1024 * 1024 for i in range(30): d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d") _insert_day(store, d, bw=bw) _insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5) # Both reads should produce same scenario rates clock1 = datetime(2026, 9, 30, 0, 0, 0, tzinfo=timezone.utc) clock2 = datetime(2026, 9, 30, 12, 0, 0, tzinfo=timezone.utc) r1 = compute_projection(store, clock1) r2 = compute_projection(store, clock2) if r1.scenario_range and r2.scenario_range: assert r1.scenario_range.rates == r2.scenario_range.rates