ci: add quality-gate workflow (semgrep, ruff, jscpd, advisory PR-Agent) #112
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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]]:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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,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"
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user