"""Tests for fenris-monitor helper. Spec: §8.4, §8.5, §8.6, §8.7 """ import json import sqlite3 from datetime import datetime, timezone from pathlib import Path from unittest.mock import patch, MagicMock import pytest import sys sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from fenris.monitor import ( cmd_enable, cmd_disable, cmd_collect, cmd_baseline_set, cmd_baseline_clear, is_root, ) from fenris.store import init_store @pytest.fixture def tmp_store(tmp_path): """Create a temporary observation store.""" store_path = tmp_path / "observations.db" conn = init_store(store_path) yield conn conn.close() @pytest.fixture def store_path(tmp_path): """Return path to a temporary observation store.""" return tmp_path / "observations.db" class TestIsRoot: def test_root_returns_true(self): with patch("os.geteuid", return_value=0): assert is_root() is True def test_non_root_returns_false(self): with patch("os.geteuid", return_value=1000): assert is_root() is False class TestEnableIdempotentMatrix: """§8.6: Period-row idempotent matrix.""" def test_first_enable_creates_missing_store(self, store_path): """A fresh package install has a store directory but no database yet.""" args = MagicMock(now=False, store_path=store_path) with patch("fenris.monitor.enable_timer") as mock_enable: cmd_enable(args) conn = init_store(store_path) row = conn.execute( "SELECT ended_at FROM monitoring_periods WHERE ended_at IS NULL" ).fetchone() conn.close() assert row is not None def test_first_opens_period(self, store_path): """First-ever enable opens a period at the enable moment.""" # Initialize store init_store(store_path) args = MagicMock(now=False, store_path=store_path) with patch("fenris.monitor.enable_timer") as mock_enable: cmd_enable(args) # Period should be open conn = init_store(store_path) cursor = conn.execute( "SELECT ended_at FROM monitoring_periods WHERE ended_at IS NULL" ) assert cursor.fetchone() is not None conn.close() def test_resume_with_open_period_noop(self, store_path): """Resume with open period: no-op (gap stays inside as unknown).""" # Initialize store and open a period conn = init_store(store_path) now = datetime.now(timezone.utc) conn.execute( "INSERT INTO monitoring_periods (started_at) VALUES (?)", (now.isoformat(),), ) conn.commit() conn.close() args = MagicMock(now=True, store_path=store_path) with patch("fenris.monitor.enable_timer") as mock_enable: cmd_enable(args) # Should still have exactly one open period conn = init_store(store_path) cursor = conn.execute( "SELECT COUNT(*) FROM monitoring_periods WHERE ended_at IS NULL" ) assert cursor.fetchone()[0] == 1 conn.close() def test_resume_with_no_period_opens_new(self, store_path): """Resume with no open period opens a new row.""" # Initialize store and close any existing period conn = init_store(store_path) conn.execute( "UPDATE monitoring_periods SET ended_at = ?, end_cause = ?", (datetime.now(timezone.utc).isoformat(), "user_disabled"), ) conn.commit() conn.close() args = MagicMock(now=True, store_path=store_path) with patch("fenris.monitor.enable_timer") as mock_enable: cmd_enable(args) # Should have a new open period conn = init_store(store_path) cursor = conn.execute( "SELECT COUNT(*) FROM monitoring_periods WHERE ended_at IS NULL" ) assert cursor.fetchone()[0] == 1 conn.close() class TestDisableIdempotentMatrix: """§8.6: Period-row idempotent matrix.""" def test_pause_with_open_period_closes_user_disabled(self, store_path): """Pause with open period closes it user_disabled.""" # Initialize store and open a period conn = init_store(store_path) now = datetime.now(timezone.utc) conn.execute( "INSERT INTO monitoring_periods (started_at) VALUES (?)", (now.isoformat(),), ) conn.commit() conn.close() args = MagicMock(now=True, store_path=store_path) with patch("fenris.monitor.disable_timer") as mock_disable: cmd_disable(args) # Period should be closed with user_disabled conn = init_store(store_path) cursor = conn.execute( "SELECT end_cause FROM monitoring_periods WHERE ended_at IS NOT NULL" ) assert cursor.fetchone()[0] == "user_disabled" conn.close() def test_pause_without_open_period_noop(self, store_path): """Pause otherwise no-ops.""" # Initialize store init_store(store_path) args = MagicMock(now=True, store_path=store_path) with patch("fenris.monitor.disable_timer") as mock_disable: cmd_disable(args) # No periods should exist conn = init_store(store_path) cursor = conn.execute("SELECT COUNT(*) FROM monitoring_periods") assert cursor.fetchone()[0] == 0 conn.close() def test_raw_systemctl_stop_never_records_user_disabled(self, store_path): """Raw systemctl stop outside helper never records user_disabled.""" # Initialize store and open a period conn = init_store(store_path) now = datetime.now(timezone.utc) conn.execute( "INSERT INTO monitoring_periods (started_at) VALUES (?)", (now.isoformat(),), ) conn.commit() # Simulate raw systemctl stop (no monitor involved) # The period stays open - only the sanctioned path closes it cursor = conn.execute( "SELECT end_cause FROM monitoring_periods WHERE ended_at IS NULL" ) assert cursor.fetchone() is not None # Still open conn.close() class TestCollectTrigger: """§8.7: On-demand collection via helper path.""" def test_collect_triggers_init_system(self): """Collect triggers init_system.collect_now.""" args = MagicMock() with patch("fenris.monitor.collect_now") as mock_collect: cmd_collect(args) mock_collect.assert_called_once() def test_collect_failure_exits_nonzero(self): """Collect failure exits with nonzero status.""" args = MagicMock() with patch("fenris.monitor.collect_now", side_effect=SystemExit(1)): with pytest.raises(SystemExit) as exc_info: cmd_collect(args) assert exc_info.value.code == 1 class TestBaselinePersistence: """PR-14: Baseline persistence behind polkit-guarded helper.""" def test_baseline_set_persists(self, store_path): """baseline set persists the baseline row.""" # Initialize store init_store(store_path) args = MagicMock( store_path=store_path, baseline_json=json.dumps( { "tbw_terabytes": 600, "source_url": "https://example.com/spec", "document_revision": "rev1", "entry_date": "2024-01-01", "model_string": "Samsung 990 Pro", "nominal_capacity_bytes": 2000000000000, } ) ) cmd_baseline_set(args) conn = init_store(store_path) cursor = conn.execute("SELECT * FROM endurance_baseline") row = cursor.fetchone() assert row is not None assert row[1] == 600.0 # tbw_terabytes conn.close() def test_baseline_set_replaces_existing(self, store_path): """baseline set replaces any existing baseline.""" # Initialize store and insert initial baseline conn = init_store(store_path) now = datetime.now(timezone.utc) conn.execute( "INSERT INTO endurance_baseline (tbw_terabytes, source_url, " "document_revision, entry_date, model_string, nominal_capacity_bytes, " "created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", (400, "old", "v1", "2023-01-01", "Old Model", 1000000000000, now.isoformat(), now.isoformat()), ) conn.commit() conn.close() args = MagicMock( store_path=store_path, baseline_json=json.dumps( { "tbw_terabytes": 600, "source_url": "new", "document_revision": "v2", "entry_date": "2024-01-01", "model_string": "New Model", "nominal_capacity_bytes": 2000000000000, } ) ) cmd_baseline_set(args) conn = init_store(store_path) cursor = conn.execute("SELECT COUNT(*) FROM endurance_baseline") assert cursor.fetchone()[0] == 1 # Only one row conn.close() def test_baseline_clear_removes(self, store_path): """baseline clear removes the baseline.""" # Initialize store and insert baseline conn = init_store(store_path) now = datetime.now(timezone.utc) conn.execute( "INSERT INTO endurance_baseline (tbw_terabytes, source_url, " "document_revision, entry_date, model_string, nominal_capacity_bytes, " "created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", (400, "src", "v1", "2024-01-01", "Model", 1000000000000, now.isoformat(), now.isoformat()), ) conn.commit() conn.close() args = MagicMock(store_path=store_path) cmd_baseline_clear(args) conn = init_store(store_path) cursor = conn.execute("SELECT COUNT(*) FROM endurance_baseline") assert cursor.fetchone()[0] == 0 conn.close()