ci: add quality-gate workflow (semgrep, ruff, jscpd, advisory PR-Agent) #112

Merged
xavierk merged 4 commits from ci/quality-gates into main 2026-10-05 14:14:59 +00:00
44 changed files with 117 additions and 213 deletions
Showing only changes of commit 3355c20202 - Show all commits
+1 -1
View File
@@ -27,7 +27,7 @@ if VENV_DIR.exists():
if site_packages: if site_packages:
sys.path.insert(0, str(site_packages)) sys.path.insert(0, str(site_packages))
from fenris.collector import run_collection from fenris.collector import run_collection # noqa: E402 -- must follow the sys.path bootstrap above
CONFIG_PATH = Path("/etc/fenris/fenris.conf") CONFIG_PATH = Path("/etc/fenris/fenris.conf")
+1 -1
View File
@@ -9,7 +9,7 @@ usage-habit classification is known rather than unknown (§5.3).
""" """
import sqlite3 import sqlite3
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
@dataclass(frozen=True) @dataclass(frozen=True)
+1 -1
View File
@@ -12,7 +12,7 @@ No proportional allocation, endpoint assignment, or double counting.
""" """
import sqlite3 import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional
from .hour_classify import classify_hour, HourSplit from .hour_classify import classify_hour, HourSplit
-1
View File
@@ -26,7 +26,6 @@ Runit guarantees:
Spec: §8.4, §8.5, §8.6, §8.7, §8.8, ADR 0008 Spec: §8.4, §8.5, §8.6, §8.7, §8.8, ADR 0008
""" """
import os import os
import shutil
import subprocess import subprocess
import sys import sys
from enum import Enum from enum import Enum
+2 -3
View File
@@ -18,11 +18,10 @@ import logging
import sqlite3 import sqlite3
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional
from .hour_classify import classify_hour from .hour_classify import classify_hour
from .segment import open_segment, normalize_identity from .segment import open_segment
from .monitoring_periods import close_period
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+3 -3
View File
@@ -31,13 +31,13 @@ if VENV_DIR.exists():
if site_packages: if site_packages:
sys.path.insert(0, str(site_packages)) sys.path.insert(0, str(site_packages))
from fenris.store import init_store, get_store_path from fenris.store import init_store # noqa: E402 -- must follow the sys.path bootstrap above
from fenris.monitoring_periods import ( from fenris.monitoring_periods import ( # noqa: E402 -- sys.path bootstrap above
ensure_period_open, ensure_period_open,
close_period, close_period,
get_open_period, get_open_period,
) )
from fenris.init_system import ( from fenris.init_system import ( # noqa: E402 -- sys.path bootstrap above
enable_timer, enable_timer,
disable_timer, disable_timer,
collect_now, collect_now,
+2 -2
View File
@@ -24,9 +24,9 @@ Criteria: PR-1–PR-17, CI-4.
""" """
import sqlite3 import sqlite3
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from enum import Enum from enum import Enum
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional
from .monitoring_periods import interval_within_one_monitoring_period from .monitoring_periods import interval_within_one_monitoring_period
+2 -2
View File
@@ -13,11 +13,11 @@ Contracts:
""" """
import logging import logging
import sqlite3 import sqlite3
from dataclasses import dataclass, field from dataclasses import dataclass
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional, List, Tuple from typing import Optional, List, Tuple
from .derive import find_previous_sample, derive_hours_from_interval, _parse_ts from .derive import derive_hours_from_interval, _parse_ts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
-1
View File
@@ -17,7 +17,6 @@ import sqlite3
from datetime import datetime from datetime import datetime
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
from .collector import normalize_identity
def find_current_segment(conn: sqlite3.Connection) -> Optional[Dict[str, Any]]: def find_current_segment(conn: sqlite3.Connection) -> Optional[Dict[str, Any]]:
+2 -3
View File
@@ -15,15 +15,14 @@ Criteria: LC-9, CI-2, CI-4, FL-4, FL-5, FL-7.
""" """
import sqlite3 import sqlite3
from contextlib import contextmanager from contextlib import contextmanager
import sys from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from .status_composition import StatusComposition from .status_composition import StatusComposition
from .projection import compute_projection, ConfidenceState, DISCLOSURES from .projection import compute_projection, DISCLOSURES
from .store import SCHEMA_VERSION from .store import SCHEMA_VERSION
from .init_system import ( from .init_system import (
query_service_state as _init_query_service_state, query_service_state as _init_query_service_state,
+4 -9
View File
@@ -11,25 +11,20 @@ Freshness grading uses shared constants from status.py.
import enum import enum
import sqlite3 import sqlite3
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone from datetime import datetime, timezone
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
# Status poll interval (AC78-6): lightweight 5s systemctl show poll
STATUS_POLL_INTERVAL_S = 5
from .status import ( from .status import (
FRESH_THRESHOLD_S,
STALENESS_THRESHOLD_S,
grade_freshness, grade_freshness,
freshness_age_human, freshness_age_human,
is_deliberately_paused, is_deliberately_paused,
monitoring_continuity, monitoring_continuity,
deliberate_pause_lines, deliberate_pause_lines,
open_store_readonly,
StoreFault,
NewerSchema,
) )
# Status poll interval (AC78-6): lightweight 5s systemctl show poll
STATUS_POLL_INTERVAL_S = 5
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Status state enum with glyph and label # Status state enum with glyph and label
-1
View File
@@ -7,7 +7,6 @@ This module handles:
""" """
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
from typing import Optional
# Schema version - increment on each migration # Schema version - increment on each migration
+9 -13
View File
@@ -12,7 +12,6 @@ Criteria: TUI-1, TUI-2, TUI-4, CI-1, CI-2, CI-4, IN-3, LC-6, LC-8.
from __future__ import annotations from __future__ import annotations
import sqlite3 import sqlite3
import sys
from datetime import date, datetime, timedelta, timezone from datetime import date, datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, List, Optional from typing import Any, Callable, Dict, List, Optional
@@ -21,7 +20,7 @@ from zoneinfo import ZoneInfo
from rich.text import Text from rich.text import Text
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.binding import Binding from textual.binding import Binding
from textual.containers import Container, Horizontal, VerticalScroll from textual.containers import Container, VerticalScroll
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.message import Message from textual.message import Message
from textual.widget import Widget from textual.widget import Widget
@@ -38,16 +37,14 @@ from .activity_plot import VolumePoint, volume_plot
from .projection import ( from .projection import (
ConfidenceState, ConfidenceState,
ProjectionResult, ProjectionResult,
ScenarioRange,
compute_projection, compute_projection,
) )
from .status import ( from .status import (
CADENCE_DEFAULT_S, CADENCE_DEFAULT_S,
FRESH_THRESHOLD_S, FRESH_THRESHOLD_S, # noqa: F401 -- re-exported; tests/test_acceptance_sweep.py asserts parity with status
STALENESS_THRESHOLD_S, STALENESS_THRESHOLD_S, # noqa: F401 -- re-exported (see above)
format_disclosures, format_disclosures,
freshness_age_human, grade_freshness, # noqa: F401 -- re-exported (see above)
grade_freshness,
deliberate_pause_lines, deliberate_pause_lines,
read_status, read_status,
_journalctl_hint, _journalctl_hint,
@@ -55,7 +52,6 @@ from .status import (
from .status_composition import ( from .status_composition import (
StatusComposition, StatusComposition,
render_status_tui, render_status_tui,
STATUS_POLL_INTERVAL_S,
) )
from .control import MONITOR_HELPER, MonitorError, run_monitor from .control import MONITOR_HELPER, MonitorError, run_monitor
from .derive import _parse_ts from .derive import _parse_ts
@@ -1110,10 +1106,10 @@ def _query_local_day_graph_data(
(start_text, end_text, selected_date or ""), (start_text, end_text, selected_date or ""),
).fetchall() ).fetchall()
recorded_zones: Dict[str, list[str]] = {} recorded_zones: Dict[str, list[str]] = {}
for local_date, timezone in rows: for local_date, zone_name in rows:
recorded_zones.setdefault(local_date, []) recorded_zones.setdefault(local_date, [])
if timezone not in recorded_zones[local_date]: if zone_name not in recorded_zones[local_date]:
recorded_zones[local_date].append(timezone) recorded_zones[local_date].append(zone_name)
result: List[Dict[str, Any]] = [] result: List[Dict[str, Any]] = []
for local_date in sorted(local_dates): for local_date in sorted(local_dates):
@@ -1138,9 +1134,9 @@ def _query_local_day_graph_data(
}) })
continue continue
for timezone in timezones: for zone_name in timezones:
summary = query_local_day_summary( summary = query_local_day_summary(
conn, local_date, timezone, now, conn, local_date, zone_name, now,
) )
if summary is None: if summary is None:
continue continue
+1 -1
View File
@@ -5,7 +5,7 @@ computation used by local-day derivation. All functions are
stateless and safe to call from the collector and TUI reader. stateless and safe to call from the collector and TUI reader.
""" """
import os import os
from datetime import datetime, timezone from datetime import datetime
from pathlib import Path from pathlib import Path
-1
View File
@@ -1,7 +1,6 @@
"""Shared test helpers for Fenris test suite.""" """Shared test helpers for Fenris test suite."""
from pathlib import Path from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent REPO_ROOT = Path(__file__).resolve().parent.parent
VERSION_FILE = REPO_ROOT / "pyproject.toml" VERSION_FILE = REPO_ROOT / "pyproject.toml"
+1 -9
View File
@@ -9,7 +9,6 @@ CI-3 Prohibition set: automated structural checks
CI-4 Required wording and six disclosures in both views CI-4 Required wording and six disclosures in both views
""" """
import re import re
import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
@@ -19,16 +18,13 @@ import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store, SCHEMA_VERSION from fenris.store import init_store
from fenris.monitoring_periods import ensure_period_open from fenris.monitoring_periods import ensure_period_open
from fenris.projection import ( from fenris.projection import (
compute_projection, compute_projection,
ConfidenceState, ConfidenceState,
BaselineTier, BaselineTier,
DISCLOSURES, DISCLOSURES,
STALENESS_HOURS,
WARMING_MIN_DAYS,
YOUNG_REGIME_DAYS,
) )
from fenris.status import ( from fenris.status import (
grade_freshness, grade_freshness,
@@ -36,13 +32,9 @@ from fenris.status import (
render_status, render_status,
format_disclosures, format_disclosures,
FRESH_THRESHOLD_S, FRESH_THRESHOLD_S,
STALENESS_THRESHOLD_S,
CADENCE_DEFAULT_S,
ACCURACY_SEC,
) )
from fenris.tui import ( from fenris.tui import (
FenrisTuiApp, FenrisTuiApp,
_format_remaining,
) )
+3 -6
View File
@@ -8,7 +8,6 @@ Seams:
- write side: run_collection() → observation store - write side: run_collection() → observation store
- read side: get_status(), compute_projection() → observation store - read side: get_status(), compute_projection() → observation store
""" """
import os
import sqlite3 import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
@@ -19,10 +18,9 @@ import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.collector import run_collection, normalize_identity from fenris.collector import run_collection
from fenris.store import init_store, get_store_path, SCHEMA_VERSION from fenris.store import init_store, SCHEMA_VERSION
from fenris.monitoring_periods import ensure_period_open, close_period, get_open_period from fenris.monitoring_periods import close_period, get_open_period
from fenris.day_aggregate import derive_day, derive_all_days
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -573,7 +571,6 @@ class TestDisplayStates:
def test_one_sample_awaiting_another_in_tui(self, tmp_path): def test_one_sample_awaiting_another_in_tui(self, tmp_path):
"""One sample → TUI shows awaiting state.""" """One sample → TUI shows awaiting state."""
from fenris.tui import FenrisTuiApp
db = tmp_path / "test.db" db = tmp_path / "test.db"
conn = init_store(db) conn = init_store(db)
conn.execute( conn.execute(
+3 -5
View File
@@ -6,13 +6,11 @@ Tests the thinnest complete write path through the system:
Seam: write side of the observation store database file. Seam: write side of the observation store database file.
""" """
import json
import os import os
import sqlite3 import sqlite3
import tempfile
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Generator from typing import Any, Dict
import pytest import pytest
@@ -20,8 +18,8 @@ import pytest
import sys import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.collector import run_collection, AcquisitionError, InvariantViolationError from fenris.collector import run_collection
from fenris.store import init_store, get_store_path from fenris.store import init_store
# Fixtures # Fixtures
+1 -3
View File
@@ -13,7 +13,6 @@ Acceptance criteria:
- Gate-3: Partial days don't satisfy the gate - Gate-3: Partial days don't satisfy the gate
- Gate-4: CLI and TUI share the same gate via compute_projection() - Gate-4: CLI and TUI share the same gate via compute_projection()
""" """
import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
@@ -24,8 +23,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store from fenris.store import init_store
from fenris.monitoring_periods import ensure_period_open from fenris.monitoring_periods import ensure_period_open
from fenris.projection import ( from fenris.projection import (
compute_projection, ConfidenceState, BaselineTier, compute_projection, ConfidenceState,
WARMING_COVERAGE_FLOOR,
) )
+1 -3
View File
@@ -6,7 +6,7 @@ shared boundary evidence, not duplicated into both days.
Seam: derive._add_unattributed_bytes() → day_aggregates.unattributed_bytes_* Seam: derive._add_unattributed_bytes() → day_aggregates.unattributed_bytes_*
""" """
import sqlite3 import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -14,8 +14,6 @@ import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.collector import run_collection from fenris.collector import run_collection
from fenris.store import init_store
from fenris.monitoring_periods import ensure_period_open
def _make_smartctl(duw: int, dur: int): def _make_smartctl(duw: int, dur: int):
+3 -4
View File
@@ -7,9 +7,8 @@ From spec §5.4, §3.3, ST-4, ST-5:
whose classification is known whose classification is known
- No absent hour ever interpolated/estimated/fabricated (FL-3) - No absent hour ever interpolated/estimated/fabricated (FL-3)
""" """
import sqlite3
import sys import sys
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -18,8 +17,8 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store from fenris.store import init_store
from fenris.monitoring_periods import ensure_period_open, close_period from fenris.monitoring_periods import ensure_period_open, close_period
from fenris.day_aggregate import derive_day, derive_all_days, DayAggregate from fenris.day_aggregate import derive_day, derive_all_days
from fenris.hour_classify import HourSplit, ACTIVE_THRESHOLD_BYTES from fenris.hour_classify import HourSplit
@pytest.fixture @pytest.fixture
+1 -2
View File
@@ -11,11 +11,10 @@ Four splits sum to exactly 3600s. Disabled time is never an hour state.
import sys import sys
from pathlib import Path from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.hour_classify import classify_hour, HourSplit, ACTIVE_THRESHOLD_BYTES from fenris.hour_classify import classify_hour, ACTIVE_THRESHOLD_BYTES
HOUR_SECONDS = 3600 HOUR_SECONDS = 3600
-1
View File
@@ -7,7 +7,6 @@ From spec §2.3:
Padded and unpadded renderings of the same field yield byte-identical stored values. Padded and unpadded renderings of the same field yield byte-identical stored values.
""" """
import pytest
import sys import sys
from pathlib import Path from pathlib import Path
+1 -7
View File
@@ -9,12 +9,9 @@ Covers:
Spec: ADR 0008, §8.4, §8.5, §8.6, §8.7, §8.8 Spec: ADR 0008, §8.4, §8.5, §8.6, §8.7, §8.8
""" """
import os
import sqlite3 import sqlite3
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import patch, MagicMock, PropertyMock from unittest.mock import patch, MagicMock
import pytest import pytest
@@ -24,7 +21,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.init_system import ( from fenris.init_system import (
InitSystem, InitSystem,
detect_init_system, detect_init_system,
get_init_system,
reset_init_system_cache, reset_init_system_cache,
_systemd_enable, _systemd_enable,
_systemd_disable, _systemd_disable,
@@ -41,8 +37,6 @@ from fenris.init_system import (
collect_now, collect_now,
query_service_state, query_service_state,
journal_hint, journal_hint,
FENRIS_SV_DIR,
FENRIS_SERVICE_LINK,
COLLECT_TIMEOUT_S, COLLECT_TIMEOUT_S,
) )
from fenris.store import init_store from fenris.store import init_store
+2 -4
View File
@@ -7,11 +7,9 @@ Covers:
- TPH-#63: Horizon anchoring at evidence endpoint - TPH-#63: Horizon anchoring at evidence endpoint
- TPH-#64: Burst/habit checks with unknown daily shares - TPH-#64: Burst/habit checks with unknown daily shares
""" """
import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
import pytest
import sys import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
@@ -19,8 +17,8 @@ from fenris.store import init_store
from fenris.monitoring_periods import ensure_period_open, close_period from fenris.monitoring_periods import ensure_period_open, close_period
from fenris.derive import derive_hours_from_interval from fenris.derive import derive_hours_from_interval
from fenris.day_aggregate import derive_day from fenris.day_aggregate import derive_day
from fenris.projection import compute_projection, ConfidenceState, STALENESS_HOURS from fenris.projection import compute_projection
from fenris.tui import _query_live_graph_data, _query_daily_graph_data from fenris.tui import _query_live_graph_data
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+2 -4
View File
@@ -6,7 +6,6 @@ These tests verify that:
3. Edge cases like zero-delta intervals and unknown daily shares are handled 3. Edge cases like zero-delta intervals and unknown daily shares are handled
4. qualifying_days_progress shows honest qualifying day count 4. qualifying_days_progress shows honest qualifying day count
""" """
import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
@@ -15,10 +14,9 @@ import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store from fenris.store import init_store
from fenris.monitoring_periods import ensure_period_open, close_period from fenris.monitoring_periods import ensure_period_open
from fenris.projection import ( from fenris.projection import (
compute_projection, ConfidenceState, BaselineTier, compute_projection, ConfidenceState,
WARMING_MIN_DAYS, WARMING_COVERAGE_FLOOR, WARMING_MAX_LOW_COVERAGE,
) )
+18 -19
View File
@@ -5,7 +5,6 @@ Covers:
- TPH-11: Single maker-credit placement, vendor wear under Drive health - TPH-11: Single maker-credit placement, vendor wear under Drive health
- Preserve: continuity, pause block, quit rail, auth banner - Preserve: continuity, pause block, quit rail, auth banner
""" """
import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
@@ -93,8 +92,8 @@ class TestFenrisIdentity:
store_path=tmp_path / "nonexistent.db", store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2, refresh_interval_s=0.2,
) )
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
headline = str(app.query_one("#headline-band").render()) str(app.query_one("#headline-band").render())
assert "🐺 Fenris by Bongbetic" in str(app.query_one("#identity").render()) assert "🐺 Fenris by Bongbetic" in str(app.query_one("#identity").render())
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -105,8 +104,8 @@ class TestFenrisIdentity:
refresh_interval_s=0.2, refresh_interval_s=0.2,
) )
# Simulate narrow terminal that can't render wolf # Simulate narrow terminal that can't render wolf
async with app.run_test(size=(60, 24)) as pilot: async with app.run_test(size=(60, 24)):
headline = str(app.query_one("#headline-band").render()) str(app.query_one("#headline-band").render())
# Either shows wolf or fallback - both are acceptable # Either shows wolf or fallback - both are acceptable
assert "Fenris by Bongbetic" in str(app.query_one("#identity").render()) assert "Fenris by Bongbetic" in str(app.query_one("#identity").render())
@@ -117,7 +116,7 @@ class TestFenrisIdentity:
store_path=tmp_path / "nonexistent.db", store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2, refresh_interval_s=0.2,
) )
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
headline = str(app.query_one("#headline-band").render()) headline = str(app.query_one("#headline-band").render())
# No replacement character (U+FFFD) should appear # No replacement character (U+FFFD) should appear
assert "\ufffd" not in headline.lower() assert "\ufffd" not in headline.lower()
@@ -137,7 +136,7 @@ class TestFenrisIdentity:
conn.close() conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db") app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
headline = str(app.query_one("#headline-band").render()) headline = str(app.query_one("#headline-band").render())
# Identity appears once, lifespan is separate data # Identity appears once, lifespan is separate data
assert "🐺 Fenris by Bongbetic" in str(app.query_one("#identity").render()) assert "🐺 Fenris by Bongbetic" in str(app.query_one("#identity").render())
@@ -160,7 +159,7 @@ class TestSingleMakerCredit:
conn.close() conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db") app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
strip = str(app.query_one("#service-strip").render()) strip = str(app.query_one("#service-strip").render())
# "by Bongbetic" should NOT appear in service strip # "by Bongbetic" should NOT appear in service strip
assert "by Bongbetic" not in strip assert "by Bongbetic" not in strip
@@ -172,15 +171,15 @@ class TestSingleMakerCredit:
store_path=tmp_path / "nonexistent.db", store_path=tmp_path / "nonexistent.db",
refresh_interval_s=0.2, refresh_interval_s=0.2,
) )
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
headline = str(app.query_one("#headline-band").render()) str(app.query_one("#headline-band").render())
assert "Fenris by Bongbetic" in str(app.query_one("#identity").render()) assert "Fenris by Bongbetic" in str(app.query_one("#identity").render())
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_empty_store_no_maker_credit_in_strip(self, tmp_path): async def test_empty_store_no_maker_credit_in_strip(self, tmp_path):
"""Empty store: no maker credit in service strip.""" """Empty store: no maker credit in service strip."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
strip = str(app.query_one("#service-strip").render()) strip = str(app.query_one("#service-strip").render())
assert "by Bongbetic" not in strip assert "by Bongbetic" not in strip
@@ -191,7 +190,7 @@ class TestSingleMakerCredit:
db = tmp_path / "test.db" db = tmp_path / "test.db"
db.write_bytes(b"not a database") db.write_bytes(b"not a database")
app = FenrisTuiApp(store_path=db) app = FenrisTuiApp(store_path=db)
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
strip = str(app.query_one("#service-strip").render()) strip = str(app.query_one("#service-strip").render())
assert "by Bongbetic" not in strip assert "by Bongbetic" not in strip
@@ -212,7 +211,7 @@ class TestDriveHealthVendorWear:
conn.close() conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db") app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
health = str(app.query_one("#drive-health").render()) health = str(app.query_one("#drive-health").render())
# Vendor wear should be present with health context # Vendor wear should be present with health context
assert "vendor wear" in health.lower() assert "vendor wear" in health.lower()
@@ -243,7 +242,7 @@ class TestDriveHealthVendorWear:
conn.close() conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db") app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
health = str(app.query_one("#drive-health").render()) health = str(app.query_one("#drive-health").render())
# Should show 0% used or honest zero, not crash # Should show 0% used or honest zero, not crash
assert "vendor wear" in health.lower() assert "vendor wear" in health.lower()
@@ -265,7 +264,7 @@ class TestPreservedBehavior:
"last_collect_ok": True, "last_collect_age_s": 60, "last_collect_ok": True, "last_collect_age_s": 60,
"last_collect_reason": None, "last_collect_reason": None,
}): }):
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
strip = str(app.query_one("#service-strip").render()) strip = str(app.query_one("#service-strip").render())
assert "continuity" in strip.lower() assert "continuity" in strip.lower()
assert "monitoring" in strip.lower() assert "monitoring" in strip.lower()
@@ -274,7 +273,7 @@ class TestPreservedBehavior:
async def test_quit_rail_preserved(self, tmp_path): async def test_quit_rail_preserved(self, tmp_path):
"""Separate q Quit TUI rail preserved.""" """Separate q Quit TUI rail preserved."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
rail = str(app.query_one("#action-rail").render()) rail = str(app.query_one("#action-rail").render())
assert "q Quit TUI" in rail assert "q Quit TUI" in rail
@@ -286,11 +285,11 @@ class TestPreservedBehavior:
refresh_interval_s=0.2, refresh_interval_s=0.2,
) )
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)) as pilot:
headline = str(app.query_one("#headline-band").render()) str(app.query_one("#headline-band").render())
assert "polkit" in str(app.query_one("#auth-notice").render()).lower() assert "polkit" in str(app.query_one("#auth-notice").render()).lower()
# Should clear after first tick # Should clear after first tick
await pilot.pause(0.25) await pilot.pause(0.25)
headline_after = str(app.query_one("#headline-band").render()) str(app.query_one("#headline-band").render())
assert not app.query_one("#auth-notice").display assert not app.query_one("#auth-notice").display
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -312,7 +311,7 @@ class TestPreservedBehavior:
"last_collect_ok": None, "last_collect_age_s": None, "last_collect_ok": None, "last_collect_age_s": None,
"last_collect_reason": None, "last_collect_reason": None,
}): }):
async with app.run_test(size=(120, 24)) as pilot: async with app.run_test(size=(120, 24)):
banner = str(app.query_one("#paused-banner").render()) banner = str(app.query_one("#paused-banner").render())
assert "paused" in banner.lower() assert "paused" in banner.lower()
assert "deliberate" in banner.lower() assert "deliberate" in banner.lower()
+10 -15
View File
@@ -8,7 +8,6 @@ Covers:
- AC80-5: Theme roles for graph rendering - AC80-5: Theme roles for graph rendering
- AC80-6: Headless interaction tests with temporary user config - AC80-6: Headless interaction tests with temporary user config
""" """
import json
import os import os
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
@@ -21,15 +20,12 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store from fenris.store import init_store
from fenris.preferences import ( from fenris.preferences import (
load_preferences,
save_preferences, save_preferences,
get_preference_path,
) )
from fenris.themes import get_theme, get_graph_colors, THEME_NAMES from fenris.themes import get_graph_colors, THEME_NAMES
from fenris.status_composition import ( from fenris.status_composition import (
StatusState, StatusState,
compose_status, compose_status,
render_status_tui,
) )
@@ -100,7 +96,7 @@ class TestPresetLoading:
config_home = _make_prefs_dir(tmp_path) config_home = _make_prefs_dir(tmp_path)
with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}): with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}):
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test() as pilot: async with app.run_test():
# Theme should be fenris-amber # Theme should be fenris-amber
assert app.theme == "fenris-chalktone" assert app.theme == "fenris-chalktone"
@@ -113,7 +109,7 @@ class TestPresetLoading:
with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}): with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}):
save_preferences(theme="nord", reduced_motion=False) save_preferences(theme="nord", reduced_motion=False)
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test() as pilot: async with app.run_test():
assert app.theme == "fenris-nord" assert app.theme == "fenris-nord"
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -125,20 +121,19 @@ class TestPresetLoading:
with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}): with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}):
save_preferences(theme="high_contrast", reduced_motion=False) save_preferences(theme="high_contrast", reduced_motion=False)
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test() as pilot: async with app.run_test():
assert app.theme == "fenris-high-contrast" assert app.theme == "fenris-high-contrast"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_tui_applies_reduced_motion_from_prefs(self, tmp_path): async def test_tui_applies_reduced_motion_from_prefs(self, tmp_path):
"""TUI respects reduced_motion preference.""" """TUI respects reduced_motion preference."""
from fenris.tui import FenrisTuiApp from fenris.tui import FenrisTuiApp
from fenris.status_composition import compose_status
config_home = _make_prefs_dir(tmp_path) config_home = _make_prefs_dir(tmp_path)
with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}): with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}):
save_preferences(theme="amber", reduced_motion=True) save_preferences(theme="amber", reduced_motion=True)
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test() as pilot: async with app.run_test():
# The app should have reduced_motion set # The app should have reduced_motion set
assert app._reduced_motion is True assert app._reduced_motion is True
@@ -237,7 +232,7 @@ class TestPersistence:
with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}): with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}):
# First run: change theme via preferences API # First run: change theme via preferences API
app1 = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app1 = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app1.run_test() as pilot: async with app1.run_test():
save_preferences(theme="nord", reduced_motion=False) save_preferences(theme="nord", reduced_motion=False)
app1._current_theme_name = "nord" app1._current_theme_name = "nord"
app1.theme = "fenris-nord" app1.theme = "fenris-nord"
@@ -246,7 +241,7 @@ class TestPersistence:
# Second run: theme should persist # Second run: theme should persist
app2 = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app2 = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app2.run_test() as pilot: async with app2.run_test():
assert app2.theme == theme_after assert app2.theme == theme_after
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -334,7 +329,7 @@ class TestReducedMotion:
config_home = _make_prefs_dir(tmp_path) config_home = _make_prefs_dir(tmp_path)
with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}): with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}):
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test() as pilot: async with app.run_test():
# The app should check for reduced motion on mount # The app should check for reduced motion on mount
assert hasattr(app, '_reduced_motion') assert hasattr(app, '_reduced_motion')
@@ -380,7 +375,7 @@ class TestSafePersistence:
with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}): with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}):
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test() as pilot: async with app.run_test():
# TUI should start with default theme # TUI should start with default theme
assert app.theme == "fenris-chalktone" assert app.theme == "fenris-chalktone"
# Dashboard should be functional # Dashboard should be functional
@@ -399,7 +394,7 @@ class TestSafePersistence:
with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}): with patch.dict(os.environ, {"XDG_CONFIG_HOME": str(config_home)}):
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test() as pilot: async with app.run_test():
# TUI should start without crash # TUI should start without crash
assert app.theme in ("fenris-chalktone", "fenris-amber", "fenris-nord", "fenris-high-contrast") assert app.theme in ("fenris-chalktone", "fenris-amber", "fenris-nord", "fenris-high-contrast")
-2
View File
@@ -10,7 +10,6 @@ Covers:
- AC92-7: Keyboard actions verified from normal launch with headless driver - AC92-7: Keyboard actions verified from normal launch with headless driver
- AC92-8: Equivalent clickable actions at 80x24 and constrained widths - AC92-8: Equivalent clickable actions at 80x24 and constrained widths
""" """
import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
@@ -111,7 +110,6 @@ def _make_app(tmp_path, clock=None):
""" """
if clock is None: if clock is None:
clock = _clock() clock = _clock()
from unittest.mock import patch
import fenris.tui as tui_mod import fenris.tui as tui_mod
from datetime import datetime as _real_datetime from datetime import datetime as _real_datetime
+3 -7
View File
@@ -11,7 +11,6 @@ Seams:
- Read side: query_local_day_history() returns entries with evidence flags - Read side: query_local_day_history() returns entries with evidence flags
- Repair: repair_derivation() does not touch existing local_days - Repair: repair_derivation() does not touch existing local_days
""" """
import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
@@ -21,7 +20,7 @@ import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store, SCHEMA_VERSION from fenris.store import init_store
from fenris.local_day import ( from fenris.local_day import (
derive_local_day_summary, derive_local_day_summary,
persist_local_day, persist_local_day,
@@ -29,11 +28,9 @@ from fenris.local_day import (
query_local_day_history, query_local_day_history,
LocalDaySummary, LocalDaySummary,
LocalDayHistoryEntry, LocalDayHistoryEntry,
_is_detail_available,
) )
from fenris.pruning import prune_old_samples, RAW_SAMPLE_RETENTION_DAYS from fenris.pruning import prune_old_samples
from fenris.repair import repair_derivation from fenris.repair import repair_derivation
from fenris.monitoring_periods import ensure_period_open, close_period
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -226,7 +223,7 @@ class TestBoundaryPreservation:
_insert_sample(conn, "2026-09-16T23:55:00+00:00", bw=100) _insert_sample(conn, "2026-09-16T23:55:00+00:00", bw=100)
_insert_sample(conn, "2026-09-17T12:30:00+00:00", bw=200) _insert_sample(conn, "2026-09-17T12:30:00+00:00", bw=200)
pruned = prune_old_samples(conn, now, retention_days=14) prune_old_samples(conn, now, retention_days=14)
# Boundary anchor should be retained # Boundary anchor should be retained
cursor = conn.execute( cursor = conn.execute(
@@ -305,7 +302,6 @@ class TestLegacyDataHandling:
def test_incomplete_legacy_labelled_correctly(self, tmp_path): def test_incomplete_legacy_labelled_correctly(self, tmp_path):
"""Legacy summary without full evidence is labelled incomplete.""" """Legacy summary without full evidence is labelled incomplete."""
conn = init_store(tmp_path / "obs.db") conn = init_store(tmp_path / "obs.db")
now = datetime(2026, 10, 1, 12, 0, 0, tzinfo=timezone.utc)
# Simulate a legacy summary that can't establish a full local-day total # Simulate a legacy summary that can't establish a full local-day total
legacy = LocalDaySummary( legacy = LocalDaySummary(
+1 -4
View File
@@ -4,9 +4,6 @@ Tests the idempotent, interruption-safe import of history.jsonl into the
observation store. observation store.
""" """
import json import json
import os
import sqlite3
import tempfile
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
@@ -17,7 +14,7 @@ import pytest
import sys import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.legacy import import_legacy_history, is_legacy_imported, _parse_history_line from fenris.legacy import import_legacy_history, _parse_history_line
from fenris.store import init_store from fenris.store import init_store
-4
View File
@@ -21,9 +21,7 @@ import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.collector import run_collection from fenris.collector import run_collection
from fenris.store import init_store
from fenris.day_aggregate import derive_day from fenris.day_aggregate import derive_day
from fenris.monitoring_periods import ensure_period_open
from fenris.tui import _query_daily_graph_data, _query_hourly_graph_data from fenris.tui import _query_daily_graph_data, _query_hourly_graph_data
@@ -247,7 +245,6 @@ class TestCrossHourAttribution:
for day in ("2026-09-01",): for day in ("2026-09-01",):
agg = derive_day(conn, day) agg = derive_day(conn, day)
assert agg is not None assert agg is not None
total_accounted = agg.bytes_written_delta + agg.bytes_read_delta
unattributed_w = conn.execute( unattributed_w = conn.execute(
"SELECT unattributed_bytes_written FROM day_aggregates WHERE day = ?", "SELECT unattributed_bytes_written FROM day_aggregates WHERE day = ?",
(day,) (day,)
@@ -483,7 +480,6 @@ class TestCrossDayBoundary:
# The cross-day delta is unattributed at the day level # The cross-day delta is unattributed at the day level
sep1 = day_map["2026-09-01"] sep1 = day_map["2026-09-01"]
sep2 = day_map["2026-09-02"]
# Both days may show the unattributed bytes # Both days may show the unattributed bytes
# (the delta is added to both days' unattributed totals as evidence) # (the delta is added to both days' unattributed totals as evidence)
total_w = sep1["allocated_bytes"] + sep1["unallocated_bytes"] total_w = sep1["allocated_bytes"] + sep1["unallocated_bytes"]
+6 -7
View File
@@ -3,7 +3,6 @@
Spec: §8.4, §8.5, §8.6, §8.7 Spec: §8.4, §8.5, §8.6, §8.7
""" """
import json import json
import sqlite3
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
@@ -56,7 +55,7 @@ class TestEnableIdempotentMatrix:
"""A fresh package install has a store directory but no database yet.""" """A fresh package install has a store directory but no database yet."""
args = MagicMock(now=False, store_path=store_path) args = MagicMock(now=False, store_path=store_path)
with patch("fenris.monitor.enable_timer") as mock_enable: with patch("fenris.monitor.enable_timer"):
cmd_enable(args) cmd_enable(args)
conn = init_store(store_path) conn = init_store(store_path)
@@ -73,7 +72,7 @@ class TestEnableIdempotentMatrix:
args = MagicMock(now=False, store_path=store_path) args = MagicMock(now=False, store_path=store_path)
with patch("fenris.monitor.enable_timer") as mock_enable: with patch("fenris.monitor.enable_timer"):
cmd_enable(args) cmd_enable(args)
# Period should be open # Period should be open
@@ -98,7 +97,7 @@ class TestEnableIdempotentMatrix:
args = MagicMock(now=True, store_path=store_path) args = MagicMock(now=True, store_path=store_path)
with patch("fenris.monitor.enable_timer") as mock_enable: with patch("fenris.monitor.enable_timer"):
cmd_enable(args) cmd_enable(args)
# Should still have exactly one open period # Should still have exactly one open period
@@ -122,7 +121,7 @@ class TestEnableIdempotentMatrix:
args = MagicMock(now=True, store_path=store_path) args = MagicMock(now=True, store_path=store_path)
with patch("fenris.monitor.enable_timer") as mock_enable: with patch("fenris.monitor.enable_timer"):
cmd_enable(args) cmd_enable(args)
# Should have a new open period # Should have a new open period
@@ -151,7 +150,7 @@ class TestDisableIdempotentMatrix:
args = MagicMock(now=True, store_path=store_path) args = MagicMock(now=True, store_path=store_path)
with patch("fenris.monitor.disable_timer") as mock_disable: with patch("fenris.monitor.disable_timer"):
cmd_disable(args) cmd_disable(args)
# Period should be closed with user_disabled # Period should be closed with user_disabled
@@ -169,7 +168,7 @@ class TestDisableIdempotentMatrix:
args = MagicMock(now=True, store_path=store_path) args = MagicMock(now=True, store_path=store_path)
with patch("fenris.monitor.disable_timer") as mock_disable: with patch("fenris.monitor.disable_timer"):
cmd_disable(args) cmd_disable(args)
# No periods should exist # No periods should exist
+1 -2
View File
@@ -6,9 +6,8 @@ From spec §5.2, §8.6, §9.8:
- Powered-off time stays inside a period; disabled time does not - Powered-off time stays inside a period; disabled time does not
- End causes: user_disabled, migrated, unknown_gap - End causes: user_disabled, migrated, unknown_gap
""" """
import sqlite3
import sys import sys
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
import pytest import pytest
+1 -5
View File
@@ -10,11 +10,11 @@ Covers:
- CLI status and collector behaviour unchanged by preferences - CLI status and collector behaviour unchanged by preferences
""" """
import json import json
from datetime import datetime, timezone
import os import os
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import pytest
import sys import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
@@ -295,7 +295,3 @@ class TestCLIIsolation:
tables_after = sorted(r[0] for r in cursor.fetchall()) tables_after = sorted(r[0] for r in cursor.fetchall())
conn.close() conn.close()
assert tables_before == tables_after assert tables_before == tables_after
# Need datetime for CLI isolation test
from datetime import datetime, timezone
+2 -6
View File
@@ -9,7 +9,6 @@ Covers acceptance criteria:
- PR-13: Baseline provenance and validation per register - 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 - 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 datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
@@ -18,11 +17,10 @@ import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store from fenris.store import init_store
from fenris.monitoring_periods import ensure_period_open, close_period from fenris.monitoring_periods import ensure_period_open
from fenris.projection import ( from fenris.projection import (
compute_projection, ConfidenceState, BaselineTier, ScenarioRange, compute_projection, ConfidenceState, BaselineTier, ScenarioRange,
TBW_TO_BYTES, HORIZON_DAYS, WARMING_MIN_DAYS, STALENESS_HOURS, TBW_TO_BYTES, DISCLOSURES,
YOUNG_REGIME_DAYS, DISCLOSURES, _compute_horizon_rate,
) )
@@ -1039,8 +1037,6 @@ class TestEvidenceAnchoredHorizons:
# cumulative_bytes = 30 * 100 * 1024 * 1024 # cumulative_bytes = 30 * 100 * 1024 * 1024
# rate = cumulative_bytes / wall_clock # rate = cumulative_bytes / wall_clock
# headline = (E_baseline - cumulative_bytes) / rate # headline = (E_baseline - cumulative_bytes) / rate
E_baseline = 10.0 * TBW_TO_BYTES
cumulative_bytes = 30 * bw
assert result.headline_remaining_seconds >= 0 assert result.headline_remaining_seconds >= 0
def test_zero_boundary_delta_returns_zero(self, store): def test_zero_boundary_delta_returns_zero(self, store):
+2 -3
View File
@@ -5,7 +5,6 @@ day aggregates are retained indefinitely.
Issue #74: Boundary anchors required for successor evidence are retained. Issue #74: Boundary anchors required for successor evidence are retained.
""" """
import sqlite3
import sys import sys
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from pathlib import Path from pathlib import Path
@@ -124,7 +123,7 @@ class TestPruneOldSamples:
_insert_sample(store_conn, "2026-09-16T12:30:00+00:00", 2000000) _insert_sample(store_conn, "2026-09-16T12:30:00+00:00", 2000000)
# Run pruning # Run pruning
pruned = prune_old_samples(store_conn, now, retention_days=14) prune_old_samples(store_conn, now, retention_days=14)
# The boundary anchor should be retained # The boundary anchor should be retained
cursor = store_conn.execute( cursor = store_conn.execute(
@@ -148,7 +147,7 @@ class TestPruneOldSamples:
store_conn.commit() store_conn.commit()
# Run pruning # Run pruning
pruned = prune_old_samples(store_conn, now, retention_days=14) prune_old_samples(store_conn, now, retention_days=14)
# Keep the source samples until local-day replacement evidence exists. # Keep the source samples until local-day replacement evidence exists.
cursor = store_conn.execute( cursor = store_conn.execute(
+1 -3
View File
@@ -58,7 +58,7 @@ def _rpm_filename(version: str, release: int = 1) -> str:
def _registry_upload_deb_url(version: str) -> str: def _registry_upload_deb_url(version: str) -> str:
"""Expected registry upload URL for a deb package.""" """Expected registry upload URL for a deb package."""
return f"debian/pool/bookworm/main/upload" return "debian/pool/bookworm/main/upload"
def _registry_upload_rpm_url() -> str: def _registry_upload_rpm_url() -> str:
@@ -258,8 +258,6 @@ class TestRevisionBumping:
"""When revision is bumped, the deb filename also changes.""" """When revision is bumped, the deb filename also changes."""
version = _get_version() version = _get_version()
# Deb filename includes release in nfpm naming # Deb filename includes release in nfpm naming
deb_r1 = f"fenris_{version}_amd64.deb"
deb_r2 = f"fenris_{version}_amd64.deb"
# For deb, the filename doesn't change with revision (deb uses epoch) # For deb, the filename doesn't change with revision (deb uses epoch)
# But the RPM does — this verifies we test RPM revision correctly # But the RPM does — this verifies we test RPM revision correctly
rpm_r1 = _rpm_filename(version, 1) rpm_r1 = _rpm_filename(version, 1)
+6 -8
View File
@@ -4,9 +4,8 @@ Tests the idempotent, safe repair of hour observations and day aggregates
from surviving raw samples, boundary anchor retention, and legacy summary from surviving raw samples, boundary anchor retention, and legacy summary
handling at actual precision. handling at actual precision.
""" """
import sqlite3
import sys import sys
from datetime import datetime, timedelta, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -102,7 +101,6 @@ class TestRepairUsesSameEvidenceRules:
def test_repair_idempotent_on_fully_derived(self, store_conn): def test_repair_idempotent_on_fully_derived(self, store_conn):
"""Repair on store with existing derived data does not duplicate.""" """Repair on store with existing derived data does not duplicate."""
now = datetime(2026, 9, 15, 12, 0, 0, tzinfo=timezone.utc)
_open_period(store_conn, "2026-09-14T00:00:00+00:00") _open_period(store_conn, "2026-09-14T00:00:00+00:00")
# Insert samples that span an hour # Insert samples that span an hour
@@ -138,7 +136,7 @@ class TestRepairUsesSameEvidenceRules:
store_conn.commit() store_conn.commit()
# Run repair # Run repair
result = repair_derivation(store_conn) repair_derivation(store_conn)
# Verify marker preserved # Verify marker preserved
cursor = store_conn.execute( cursor = store_conn.execute(
@@ -265,7 +263,7 @@ class TestBoundaryAnchorRetention:
_insert_sample(store_conn, "2026-09-29T12:00:00+00:00", 3000000) _insert_sample(store_conn, "2026-09-29T12:00:00+00:00", 3000000)
# Run pruning # Run pruning
pruned = prune_old_samples(store_conn, now, retention_days=14) prune_old_samples(store_conn, now, retention_days=14)
# The boundary anchor should be retained # The boundary anchor should be retained
cursor = store_conn.execute( cursor = store_conn.execute(
@@ -286,7 +284,7 @@ class TestBoundaryAnchorRetention:
bytes_written_delta=1000000) bytes_written_delta=1000000)
# Run pruning # Run pruning
pruned = prune_old_samples(store_conn, now, retention_days=14) prune_old_samples(store_conn, now, retention_days=14)
# The source remains until local-day evidence is also durable. # The source remains until local-day evidence is also durable.
cursor = store_conn.execute( cursor = store_conn.execute(
@@ -309,7 +307,7 @@ class TestLegacySummaryPrecision:
bytes_written_delta=5000000) bytes_written_delta=5000000)
# Run repair # Run repair
result = repair_derivation(store_conn) repair_derivation(store_conn)
# Should not create hour observations for legacy day # Should not create hour observations for legacy day
cursor = store_conn.execute( cursor = store_conn.execute(
@@ -324,7 +322,7 @@ class TestLegacySummaryPrecision:
bytes_written_delta=5000000) bytes_written_delta=5000000)
# Run repair # Run repair
result = repair_derivation(store_conn) repair_derivation(store_conn)
# Day aggregate should not be modified # Day aggregate should not be modified
cursor = store_conn.execute( cursor = store_conn.execute(
+2 -4
View File
@@ -11,13 +11,11 @@ Covers:
- Retired command rejection with migration pointers - Retired command rejection with migration pointers
- Configuration error surfaced from direct reads - Configuration error surfaced from direct reads
""" """
import os
import sqlite3 import sqlite3
import sys import sys
import tempfile
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch
import pytest import pytest
@@ -35,7 +33,7 @@ from fenris.status import (
ACCURACY_SEC, ACCURACY_SEC,
) )
from fenris.store import init_store, SCHEMA_VERSION from fenris.store import init_store, SCHEMA_VERSION
from fenris.projection import DISCLOSURES, ConfidenceState from fenris.projection import DISCLOSURES
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+4 -9
View File
@@ -10,10 +10,9 @@ Covers:
- AC78-5: Separate freshness, last outcome, boot enablement, activity facts - AC78-5: Separate freshness, last outcome, boot enablement, activity facts
- AC78-6: 5s poll reevaluates freshness from cached clock - AC78-6: 5s poll reevaluates freshness from cached clock
""" """
import sqlite3
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch
import pytest import pytest
import sys import sys
@@ -23,12 +22,9 @@ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import init_store from fenris.store import init_store
from fenris.status_composition import ( from fenris.status_composition import (
StatusState, StatusState,
StatusComposition,
compose_status, compose_status,
render_status_cli, render_status_cli,
render_status_tui, render_status_tui,
FRESH_THRESHOLD_S,
STALENESS_THRESHOLD_S,
) )
@@ -682,7 +678,6 @@ class TestStoreFaultSuppression:
conn.close() conn.close()
def test_newer_schema_suppresses_projection(self, tmp_path): def test_newer_schema_suppresses_projection(self, tmp_path):
import fenris.store as store_mod
conn = init_store(tmp_path / "test.db") conn = init_store(tmp_path / "test.db")
now = _clock() now = _clock()
@@ -971,7 +966,7 @@ class TestTUIIntegration:
"last_collect_ok": True, "last_collect_age_s": 120, "last_collect_ok": True, "last_collect_age_s": 120,
"last_collect_reason": None, "last_collect_reason": None,
}): }):
async with app.run_test() as pilot: async with app.run_test():
strip = str(app.query_one("#service-strip").render()) strip = str(app.query_one("#service-strip").render())
assert "●" in strip assert "●" in strip
assert "Monitoring" in strip or "monitoring" in strip.lower() assert "Monitoring" in strip or "monitoring" in strip.lower()
@@ -997,7 +992,7 @@ class TestTUIIntegration:
"last_collect_ok": None, "last_collect_age_s": None, "last_collect_ok": None, "last_collect_age_s": None,
"last_collect_reason": None, "last_collect_reason": None,
}): }):
async with app.run_test() as pilot: async with app.run_test():
strip = str(app.query_one("#service-strip").render()) strip = str(app.query_one("#service-strip").render())
# Should show Paused state # Should show Paused state
assert "‖" in strip or "Paused" in strip or "paused" in strip.lower() assert "‖" in strip or "Paused" in strip or "paused" in strip.lower()
@@ -1024,7 +1019,7 @@ class TestTUIIntegration:
"last_collect_ok": False, "last_collect_age_s": 60, "last_collect_ok": False, "last_collect_age_s": 60,
"last_collect_reason": "exit code 3", "last_collect_reason": "exit code 3",
}): }):
async with app.run_test() as pilot: async with app.run_test():
strip = str(app.query_one("#service-strip").render()) strip = str(app.query_one("#service-strip").render())
# Should show Error state # Should show Error state
assert "✖" in strip or "Error" in strip or "error" in strip.lower() assert "✖" in strip or "Error" in strip or "error" in strip.lower()
+1 -2
View File
@@ -6,7 +6,6 @@ instead of degrading to the Store fault view; (2) even group members could
not open the WAL-mode store because root-created sidecars lacked group write not open the WAL-mode store because root-created sidecars lacked group write
and the store directory lacked group execute-then-write. and the store directory lacked group execute-then-write.
""" """
import sqlite3
import sys import sys
from pathlib import Path from pathlib import Path
@@ -14,7 +13,7 @@ import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from fenris.store import DEFAULT_STORE_PATH, init_store from fenris.store import init_store
from fenris.status import StoreFault, open_store_readonly from fenris.status import StoreFault, open_store_readonly
-1
View File
@@ -7,7 +7,6 @@ Covers:
- Status semantic colours/glyphs/text always win over theme - Status semantic colours/glyphs/text always win over theme
- Global action reachability and focus contrast in every preset - Global action reachability and focus contrast in every preset
""" """
import pytest
import sys import sys
from pathlib import Path from pathlib import Path
+13 -21
View File
@@ -13,29 +13,22 @@ import sqlite3
from xml.etree import ElementTree from xml.etree import ElementTree
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import patch, MagicMock from unittest.mock import patch
import pytest import pytest
import sys import sys
sys.path.insert(0, str(Path(__file__).parent.parent / "src")) sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from textual.app import App
from textual.pilot import Pilot
from fenris.store import init_store, SCHEMA_VERSION from fenris.store import init_store
from fenris.monitoring_periods import ensure_period_open, close_period from fenris.monitoring_periods import ensure_period_open
from fenris.projection import ( from fenris.projection import (
ConfidenceState, ConfidenceState,
compute_projection, compute_projection,
DISCLOSURES,
WARMING_MIN_DAYS,
STALENESS_HOURS,
YOUNG_REGIME_DAYS,
) )
from fenris.status import ( from fenris.status import (
FRESH_THRESHOLD_S, FRESH_THRESHOLD_S,
STALENESS_THRESHOLD_S,
grade_freshness, grade_freshness,
) )
from fenris.tui import ( from fenris.tui import (
@@ -45,7 +38,6 @@ from fenris.tui import (
_query_drive_health, _query_drive_health,
_query_daily_graph_data, _query_daily_graph_data,
_query_hourly_graph_data, _query_hourly_graph_data,
_RANGE_OPTIONS,
_RANGE_DEFAULT, _RANGE_DEFAULT,
_MIN_WIDTH, _MIN_WIDTH,
_MIN_HEIGHT, _MIN_HEIGHT,
@@ -283,7 +275,7 @@ class TestDenseScreen:
conn.close() conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db") app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test() as pilot: async with app.run_test():
assert app.query_one("#headline-band") is not None assert app.query_one("#headline-band") is not None
assert app.query_one("#usage-history") is not None assert app.query_one("#usage-history") is not None
assert app.query_one("#drive-health") is not None assert app.query_one("#drive-health") is not None
@@ -303,7 +295,7 @@ class TestDenseScreen:
conn.close() conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db") app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test() as pilot: async with app.run_test():
headline = str(app.query_one("#headline-band").render()) headline = str(app.query_one("#headline-band").render())
assert "remaining" in headline.lower() or "projection" in headline.lower() assert "remaining" in headline.lower() or "projection" in headline.lower()
@@ -321,7 +313,7 @@ class TestDenseScreen:
conn.close() conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db") app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test() as pilot: async with app.run_test():
headline = str(app.query_one("#headline-band").render()) headline = str(app.query_one("#headline-band").render())
assert "projection confidence" in headline.lower() assert "projection confidence" in headline.lower()
# Contributing facts shown, never a percentage as confidence # Contributing facts shown, never a percentage as confidence
@@ -343,7 +335,7 @@ class TestDenseScreen:
conn.close() conn.close()
app = FenrisTuiApp(store_path=tmp_path / "test.db") app = FenrisTuiApp(store_path=tmp_path / "test.db")
async with app.run_test() as pilot: async with app.run_test():
strip = str(app.query_one("#service-strip").render()) strip = str(app.query_one("#service-strip").render())
assert "Boot:" in strip assert "Boot:" in strip
assert "Timer:" in strip assert "Timer:" in strip
@@ -484,7 +476,7 @@ class TestDenseScreen:
auth_notice = "Open with fenris (no sudo). Actions authenticate via polkit. ? Help" auth_notice = "Open with fenris (no sudo). Actions authenticate via polkit. ? Help"
async with app.run_test() as pilot: async with app.run_test() as pilot:
headline = str(app.query_one("#headline-band").render()) str(app.query_one("#headline-band").render())
# Identity now shows Fenris by Bongbetic (issue #79) # Identity now shows Fenris by Bongbetic (issue #79)
assert "Fenris by Bongbetic" in str(app.query_one("#identity").render()) assert "Fenris by Bongbetic" in str(app.query_one("#identity").render())
assert auth_notice in str(app.query_one("#auth-notice").render()) assert auth_notice in str(app.query_one("#auth-notice").render())
@@ -526,7 +518,7 @@ class TestDisclosuresAndGreeting:
async def test_empty_store_greeting(self, tmp_path): async def test_empty_store_greeting(self, tmp_path):
"""Empty store shows 'no observations yet' with enable hint.""" """Empty store shows 'no observations yet' with enable hint."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test() as pilot: async with app.run_test():
headline = str(app.query_one("#headline-band").render()) headline = str(app.query_one("#headline-band").render())
assert "no observations yet" in headline.lower() assert "no observations yet" in headline.lower()
assert "enable" in headline.lower() or "resume" in headline.lower() assert "enable" in headline.lower() or "resume" in headline.lower()
@@ -591,9 +583,9 @@ class TestFirstRun:
async def test_first_run_prompt(self, tmp_path): async def test_first_run_prompt(self, tmp_path):
"""First-run prompt makes the keyboard action and boot effect explicit.""" """First-run prompt makes the keyboard action and boot effect explicit."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test() as pilot: async with app.run_test():
headline = str(app.query_one("#headline-band").render()) headline = str(app.query_one("#headline-band").render())
strip = str(app.query_one("#service-strip").render()) str(app.query_one("#service-strip").render())
assert "no observations yet" in headline.lower() assert "no observations yet" in headline.lower()
assert "r resume — enable monitoring and future boots" in headline.lower() assert "r resume — enable monitoring and future boots" in headline.lower()
assert "r resume" in str(app.query_one("#action-rail").render()).lower() assert "r resume" in str(app.query_one("#action-rail").render()).lower()
@@ -866,7 +858,7 @@ class TestQueryHourlyGraphData:
class TestHistoryGraph: class TestHistoryGraph:
def test_empty_data(self, tmp_path): def test_empty_data(self, tmp_path):
"""Empty data shows awaiting message.""" """Empty data shows awaiting message."""
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
# We test the widget directly via the app's compose # We test the widget directly via the app's compose
graph = HistoryGraph() graph = HistoryGraph()
# Simulate setting empty data # Simulate setting empty data
@@ -1129,7 +1121,7 @@ class TestBarGraphTUI:
app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db") app = FenrisTuiApp(store_path=tmp_path / "nonexistent.db")
async with app.run_test(size=(80, 24)) as pilot: async with app.run_test(size=(80, 24)) as pilot:
await pilot.press("v", "v") await pilot.press("v", "v")
graph = app.query_one("#usage-history") app.query_one("#usage-history")
assert app.query_one("#activity-panel").border_title == "Drive activity" assert app.query_one("#activity-panel").border_title == "Drive activity"
@pytest.mark.asyncio @pytest.mark.asyncio