"""Integration: each rolled-up hour keeps the level it held (#5738). A learned normal has to tell a two-minute backup spike from an hour spent at that level. Both have the same peak; only the hour's 95th percentile differs, so the rollup must keep it beside the average and the peak. Requires STEWARD_DATABASE_URL. """ from __future__ import annotations import asyncio import os import uuid from datetime import datetime, timedelta, timezone import pytest pytestmark = pytest.mark.integration @pytest.fixture def app(): if not os.environ.get("STEWARD_DATABASE_URL"): pytest.skip("needs Postgres") from steward.app import create_app return create_app(testing=False) def test_a_brief_spike_raises_the_peak_and_a_held_level_raises_the_p95(app): from sqlalchemy import select from steward.core.metrics_retention import rollup_plugin_metrics from steward.models.metrics import PluginMetric, PluginMetricHourly now = datetime.now(timezone.utc) res = f"p95-{uuid.uuid4().hex[:8]}" spike_hour = (now - timedelta(days=10)).replace(minute=0, second=0, microsecond=0) held_hour = spike_hour + timedelta(hours=1) def minute(hour, m, metric, value): return PluginMetric(source_module="test", resource_name=res, metric_name=metric, value=value, recorded_at=hour + timedelta(minutes=m)) async def _go(): async with app.db_sessionmaker() as s: async with s.begin(): # One sample a minute. The spike hour sits at 10 and touches 95 # for two minutes; the held hour sits at 90 bar a two-minute dip. s.add_all([minute(spike_hour, m, "cpu_pct", 95.0 if m in (2, 3) else 10.0) for m in range(60)]) s.add_all([minute(held_hour, m, "cpu_pct", 10.0 if m in (2, 3) else 90.0) for m in range(60)]) async with s.begin(): await rollup_plugin_metrics(s, raw_days=7, rollup_days=90, now=now) rows = (await s.execute( select(PluginMetricHourly) .where(PluginMetricHourly.resource_name == res) .order_by(PluginMetricHourly.bucket))).scalars().all() return [(r.bucket, r.value_max, r.value_p95, r.value_avg) for r in rows] (b1, spike_max, spike_p95, _), (b2, held_max, held_p95, _) = asyncio.run(_go()) assert (b1, b2) == (spike_hour, held_hour) assert spike_max == 95.0 and abs(spike_p95 - 10.0) < 0.001 assert held_max == 90.0 and abs(held_p95 - 90.0) < 0.001