319 lines
13 KiB
Python
319 lines
13 KiB
Python
"""Tests for issue #79: Apply Fenris identity and Drive health grouping.
|
|
|
|
Covers:
|
|
- TPH-1: Titlebox shows Fenris identity with wolf fallback
|
|
- TPH-11: Single maker-credit placement, vendor wear under Drive health
|
|
- Preserve: continuity, pause block, quit rail, auth banner
|
|
"""
|
|
import sqlite3
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
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
|
|
from fenris.tui import FenrisTuiApp
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers (reuse from test_tui.py)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
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"):
|
|
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, "https://example.com/spec", "v1.0", "2026-01-01", model,
|
|
1024000000000, "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, device="/dev/nvme0n1"):
|
|
conn.execute(
|
|
"INSERT INTO samples (ts, device, data_units_written, data_units_read, "
|
|
"percentage_used, bytes_written, bytes_read, power_on_hours) "
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
(ts, device, 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))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Issue #79: Fenris identity and Drive health grouping
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestFenrisIdentity:
|
|
"""TPH-1: Titlebox shows Fenris identity with wolf fallback."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_titlebox_shows_wolf_identity(self, tmp_path):
|
|
"""Top titlebox reads 🐺 Fenris by Bongbetic."""
|
|
app = FenrisTuiApp(
|
|
store_path=tmp_path / "nonexistent.db",
|
|
refresh_interval_s=0.2,
|
|
)
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
headline = str(app.query_one("#headline-band").render())
|
|
assert "🐺 Fenris by Bongbetic" in str(app.query_one("#identity").render())
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_titlebox_fallback_without_wolf(self, tmp_path):
|
|
"""Fallback to 'Fenris by Bongbetic' when wolf is unsupported."""
|
|
app = FenrisTuiApp(
|
|
store_path=tmp_path / "nonexistent.db",
|
|
refresh_interval_s=0.2,
|
|
)
|
|
# Simulate narrow terminal that can't render wolf
|
|
async with app.run_test(size=(60, 24)) as pilot:
|
|
headline = str(app.query_one("#headline-band").render())
|
|
# Either shows wolf or fallback - both are acceptable
|
|
assert "Fenris by Bongbetic" in str(app.query_one("#identity").render())
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wolf_never_shows_tofu(self, tmp_path):
|
|
"""Wolf glyph is never rendered as tofu (unsupported character)."""
|
|
app = FenrisTuiApp(
|
|
store_path=tmp_path / "nonexistent.db",
|
|
refresh_interval_s=0.2,
|
|
)
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
headline = str(app.query_one("#headline-band").render())
|
|
# No replacement character (U+FFFD) should appear
|
|
assert "\ufffd" not in headline.lower()
|
|
assert "?" not in headline or "Fenris" in headline
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lifespan_headline_is_data_surface(self, tmp_path):
|
|
"""Lifespan headline remains a data surface, not app title."""
|
|
conn = init_store(tmp_path / "test.db")
|
|
_insert_baseline(conn, tbw_tb=1.0, verified=True)
|
|
_insert_segment(conn)
|
|
_open_period(conn)
|
|
for i in range(30):
|
|
d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d")
|
|
_insert_day(conn, d, bw=1024*1024*100, coverage=0.95, samples=24)
|
|
_insert_sample(conn, "2026-09-30T10:00:00+00:00")
|
|
conn.close()
|
|
|
|
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
headline = str(app.query_one("#headline-band").render())
|
|
# Identity appears once, lifespan is separate data
|
|
assert "🐺 Fenris by Bongbetic" in str(app.query_one("#identity").render())
|
|
assert "remaining" in headline.lower() or "projection" in headline.lower()
|
|
|
|
|
|
class TestSingleMakerCredit:
|
|
"""TPH-1: Single maker-credit placement in title only."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_maker_credit_not_in_service_strip(self, tmp_path):
|
|
"""Remove duplicate maker credit from service facts."""
|
|
conn = init_store(tmp_path / "test.db")
|
|
_insert_segment(conn)
|
|
_open_period(conn)
|
|
for i in range(20):
|
|
d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d")
|
|
_insert_day(conn, d, bw=1024*1024*100)
|
|
_insert_sample(conn, "2026-09-30T10:00:00+00:00")
|
|
conn.close()
|
|
|
|
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
strip = str(app.query_one("#service-strip").render())
|
|
# "by Bongbetic" should NOT appear in service strip
|
|
assert "by Bongbetic" not in strip
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_maker_credit_in_titlebox(self, tmp_path):
|
|
"""Maker credit appears in the titlebox identity."""
|
|
app = FenrisTuiApp(
|
|
store_path=tmp_path / "nonexistent.db",
|
|
refresh_interval_s=0.2,
|
|
)
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
headline = str(app.query_one("#headline-band").render())
|
|
assert "Fenris by Bongbetic" in str(app.query_one("#identity").render())
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_store_no_maker_credit_in_strip(self, tmp_path):
|
|
"""Empty store: no maker credit in service strip."""
|
|
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
strip = str(app.query_one("#service-strip").render())
|
|
assert "by Bongbetic" not in strip
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_store_fault_no_maker_credit_in_strip(self, tmp_path):
|
|
"""Store fault: no maker credit in service strip."""
|
|
# Create a corrupt store
|
|
db = tmp_path / "test.db"
|
|
db.write_bytes(b"not a database")
|
|
app = FenrisTuiApp(store_path=db)
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
strip = str(app.query_one("#service-strip").render())
|
|
assert "by Bongbetic" not in strip
|
|
|
|
|
|
class TestDriveHealthVendorWear:
|
|
"""TPH-1: Vendor wear renders under Drive health context."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_vendor_wear_in_drive_health(self, tmp_path):
|
|
"""Vendor wear shows with health context, not as Settings."""
|
|
conn = init_store(tmp_path / "test.db")
|
|
_insert_segment(conn)
|
|
_open_period(conn)
|
|
for i in range(20):
|
|
d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d")
|
|
_insert_day(conn, d, bw=1024*1024*100)
|
|
_insert_sample(conn, "2026-09-30T10:00:00+00:00", pu=10)
|
|
conn.close()
|
|
|
|
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
health = str(app.query_one("#drive-health").render())
|
|
# Vendor wear should be present with health context
|
|
assert "vendor wear" in health.lower()
|
|
# Should show thermal, spare, errors, etc.
|
|
assert "temperature" in health.lower()
|
|
assert "spare" in health.lower()
|
|
assert "media errors" in health.lower()
|
|
# Settings section should NOT be a separate heading
|
|
assert "[bold]Settings[/bold]" not in health
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_wear_values_honest(self, tmp_path):
|
|
"""Missing values remain honest unavailable facts."""
|
|
conn = init_store(tmp_path / "test.db")
|
|
_insert_segment(conn)
|
|
_open_period(conn)
|
|
for i in range(20):
|
|
d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d")
|
|
_insert_day(conn, d, bw=1024*1024*100)
|
|
# Insert sample with zero wear values
|
|
conn.execute(
|
|
"INSERT INTO samples (ts, device, data_units_written, data_units_read, "
|
|
"percentage_used, bytes_written, bytes_read, power_on_hours) "
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
("2026-09-30T10:00:00+00:00", "/dev/nvme0n1", 0, 0, 0, 0, 0, 0),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
health = str(app.query_one("#drive-health").render())
|
|
# Should show 0% used or honest zero, not crash
|
|
assert "vendor wear" in health.lower()
|
|
|
|
|
|
class TestPreservedBehavior:
|
|
"""Preserve existing continuity, pause, quit, auth behavior."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_continuity_preserved(self, tmp_path):
|
|
"""Continuity wording preserved in service strip."""
|
|
conn = init_store(tmp_path / "test.db")
|
|
_open_period(conn)
|
|
conn.close()
|
|
app = FenrisTuiApp(store_path=tmp_path / "test.db")
|
|
|
|
with patch("fenris.status.query_service_state", return_value={
|
|
"boot_enabled": True, "timer_active": True,
|
|
"last_collect_ok": True, "last_collect_age_s": 60,
|
|
"last_collect_reason": None,
|
|
}):
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
strip = str(app.query_one("#service-strip").render())
|
|
assert "continuity" in strip.lower()
|
|
assert "monitoring" in strip.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_quit_rail_preserved(self, tmp_path):
|
|
"""Separate q Quit TUI rail preserved."""
|
|
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
rail = str(app.query_one("#action-rail").render())
|
|
assert "q Quit TUI" in rail
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_auth_banner_preserved(self, tmp_path):
|
|
"""Polkit authentication banner lifecycle preserved."""
|
|
app = FenrisTuiApp(
|
|
store_path=tmp_path / "nonexistent.db",
|
|
refresh_interval_s=0.2,
|
|
)
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
headline = str(app.query_one("#headline-band").render())
|
|
assert "polkit" in str(app.query_one("#auth-notice").render()).lower()
|
|
# Should clear after first tick
|
|
await pilot.pause(0.25)
|
|
headline_after = str(app.query_one("#headline-band").render())
|
|
assert not app.query_one("#auth-notice").display
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_deliberate_pause_block_preserved(self, tmp_path):
|
|
"""Deliberate pause banner preserved."""
|
|
db = tmp_path / "test.db"
|
|
conn = init_store(db)
|
|
conn.execute(
|
|
"INSERT INTO monitoring_periods (started_at, ended_at, end_cause) "
|
|
"VALUES (?, ?, ?)",
|
|
("2026-09-01T09:00:00+00:00", "2026-09-01T10:00:00+00:00", "user_disabled"),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
app = FenrisTuiApp(store_path=db)
|
|
|
|
with patch("fenris.status.query_service_state", return_value={
|
|
"boot_enabled": False, "timer_active": False,
|
|
"last_collect_ok": None, "last_collect_age_s": None,
|
|
"last_collect_reason": None,
|
|
}):
|
|
async with app.run_test(size=(120, 24)) as pilot:
|
|
banner = str(app.query_one("#paused-banner").render())
|
|
assert "paused" in banner.lower()
|
|
assert "deliberate" in banner.lower()
|