Files
FabledSteward/tests/integration/test_cleanup.py
T
bvandeusenandClaude Opus 5.5 0b2fbf13fc
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m12s
CI / unit (push) Successful in 1m28s
CI / publish (push) Successful in 1m4s
feat(settings): Settings → System shows table sizes and what the last cleanup removed (#5469)
#5469 "Settings → System: table sizes and what the last cleanup removed".

- Storage: each table's size on disk (pg_total_relation_size, indexes and TOAST
  included) and Postgres's row estimate (reltuples), largest first, the 15
  largest by name and the rest summed, under the database's total size. Read
  from the catalog only, never by counting rows.
- Last cleanup: when it ran, how long it took, and what each pass removed (each
  aged table, run log files, the metric and Docker rollups, the log ring), or
  "Nothing was past its window". run_cleanup now returns that summary and keeps
  it as the cleanup.last_run setting, as reports.last_sent_at does; cleanup_secs
  (#5467) is recorded from the same figure.
- A hint links to where each retention window is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:29:55 -04:00

103 lines
4.2 KiB
Python

"""Integration: retention windows read from Settings reach the pruning (#5155)."""
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_the_audit_window_comes_from_settings(app):
from sqlalchemy import text
from steward.core.cleanup import run_cleanup
from steward.core.settings import get_setting, set_setting
from steward.models.audit import AuditEvent
now = datetime.now(timezone.utc)
tag = f"test.cleanup.{uuid.uuid4().hex[:8]}"
async def _go():
async with app.db_sessionmaker() as s:
async with s.begin():
await set_setting(s, "audit.retention_days", 30)
s.add(AuditEvent(action=tag, entity_id="old", timestamp=now - timedelta(days=40)))
s.add(AuditEvent(action=tag, entity_id="new", timestamp=now - timedelta(days=20)))
try:
done = await run_cleanup(app)
finally:
async with app.db_sessionmaker() as s:
async with s.begin():
await set_setting(s, "audit.retention_days", 365)
async with app.db_sessionmaker() as s:
kept = [r[0] for r in (await s.execute(text(
"SELECT entity_id FROM audit_events WHERE action = :a"), {"a": tag})).all()]
return kept, done, await get_setting(s, "cleanup.last_run")
kept, done, stored = asyncio.run(_go())
assert kept == ["new"]
# What it did is kept for Settings → System (#5469), each pass by name.
counts = dict(done["removed"])
assert counts["audit_events: older than 30 days"] >= 1
assert "plugin_metrics: raw samples rolled into hourly" in counts and done["secs"] >= 0
assert stored == done
def test_the_chart_seam_follows_the_raw_metrics_setting(app):
# With raw_days 1, a range two days back reads the hourly rollup there and
# not the raw sample beside it; at the default 7 it would read the raw one.
from steward.core import metric_series
from steward.core.settings import set_setting
from steward.models.metrics import PluginMetric, PluginMetricHourly
now = datetime.now(timezone.utc)
two_days = (now - timedelta(days=2)).replace(minute=0, second=0, microsecond=0)
res = f"seam-{uuid.uuid4().hex[:8]}"
async def _go():
async with app.db_sessionmaker() as s:
async with s.begin():
await set_setting(s, "metrics.retention.raw_days", 1)
s.add(PluginMetricHourly(source_module="test", resource_name=res,
metric_name="cpu_pct", bucket=two_days,
value_avg=10.0, value_max=10.0, sample_count=1))
s.add(PluginMetric(source_module="test", resource_name=res,
metric_name="cpu_pct", value=99.0, recorded_at=two_days))
try:
async with app.db_sessionmaker() as s:
return await metric_series.points(s, "test", res, "cpu_pct",
now - timedelta(days=3))
finally:
async with app.db_sessionmaker() as s:
async with s.begin():
await set_setting(s, "metrics.retention.raw_days", 7)
assert [v for _, v in asyncio.run(_go())] == [10.0]
def test_table_sizes_come_from_the_catalog_largest_first(app):
"""Settings → System's storage figures (#5469), read against real Postgres."""
from steward.core.health import TABLES_SHOWN, table_sizes
async def _go():
async with app.db_sessionmaker() as s:
return await table_sizes(s)
got = asyncio.run(_go())
sizes = [t["bytes"] for t in got["tables"]]
assert sizes == sorted(sizes, reverse=True) and len(sizes) <= TABLES_SHOWN
names = {t["name"] for t in got["tables"]}
assert got["rest"]["count"] or "audit_events" in names # the schema's own tables
assert got["total"] >= sum(sizes) + got["rest"]["bytes"] > 0