diff --git a/MAINTAINERS.md b/MAINTAINERS.md index f2e937d8..8b0fff79 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -90,8 +90,10 @@ When you are ready to cut a new release, follow these steps to generate the tbump X.Y.Z --no-push ``` - _Note: `tbump` will automatically trigger the `before_commit` hook, run - `update-package-metadata.py`, generate the changelog and bundle everything + _Note: `tbump` will automatically trigger the `before_commit` hooks to + regenerate the status screenshot + (`scripts/maintainer/generate-screenshot.py`), run + `update-package-metadata.py`, generate the changelog, and bundle everything into a single release commit and local tag._ 💡 **Tip for Release Candidates:** You can safely use standard SemVer tags diff --git a/README.md b/README.md index 639dafdf..e3a75987 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ updates. It features Snapper-integrated atomic rollbacks, multi-channel alerting user-level execution. Designed for reliability and ease of use, it ensures your Flatpak environment remains current and resilient. + + +
Flatpak Automatic
+System Status Overview
+ + ## ✨ Features - **Automated Flatpak Updates:** Keep your flatpak applications up-to-date @@ -154,8 +160,8 @@ first run). To trigger an update manually or use the advanced CLI: ```text -usage: flatpak-automatic [-h] [-d] [-t] [-f] [-s] [-l] [-a] [-c] [-r] - [--desktop-mode] [-e] [-x] +usage: flatpak-automatic [-h] [-d] [-t] [-f] [-s] [-B] [-v] [-V] [-l] [-a] [-c] + [-r] [--desktop-mode] [-e] [-x] Flatpak Automatic - Advanced Update Automation @@ -165,6 +171,9 @@ options: -t, --test-notify Send a test notification to configured endpoints and exit. -f, --force Force the update process, ignoring safeguards. -s, --status Display system monitoring overview and exit. + -B, --hide-banner Hide the header ASCII banner upon execution. + -v, --version Show application version and exit. + -V, --verbose Enable verbose output (e.g. detailed status view with -s/--status). -l, --history Display recent update history from journalctl and exit. -a, --apply-schedule Apply systemd timer overrides based on config settings. -c, --check-config Validate and print the current configuration, then exit. diff --git a/assets/status-screenshot.png b/assets/status-screenshot.png new file mode 100644 index 00000000..26a68567 Binary files /dev/null and b/assets/status-screenshot.png differ diff --git a/config/config.default.yaml b/config/config.default.yaml index 095a5e84..0710701c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -38,6 +38,15 @@ exclusions: [] # Type: string, Default: always auto_notify: always +# ------------------------------------------------------------------------------ +# CLI & Interface Settings +# ------------------------------------------------------------------------------ + +cli: + # Hide the ASCII header banner when executing from terminal. + # Type: boolean, Default: false + hide_banner: false + # ------------------------------------------------------------------------------ # Systemd Timer Schedule # ------------------------------------------------------------------------------ diff --git a/config/config.example.yaml b/config/config.example.yaml index 10ee4483..49dd2f48 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -31,6 +31,14 @@ exclusions: # Notification trigger policy: 'always', 'on-change', 'on-failure', or 'never'. auto_notify: always +# ------------------------------------------------------------------------------ +# CLI & Interface Settings +# ------------------------------------------------------------------------------ + +cli: + # Hide the ASCII header banner when executing from terminal. + hide_banner: false + # ------------------------------------------------------------------------------ # Systemd Timer Schedule # ------------------------------------------------------------------------------ diff --git a/config/config.user.default.yaml b/config/config.user.default.yaml index 2af14165..889829e0 100644 --- a/config/config.user.default.yaml +++ b/config/config.user.default.yaml @@ -32,6 +32,15 @@ exclusions: [] # Type: string, Default: always auto_notify: always +# ------------------------------------------------------------------------------ +# CLI & Interface Settings +# ------------------------------------------------------------------------------ + +cli: + # Hide the ASCII header banner when executing from terminal. + # Type: boolean, Default: false + hide_banner: false + # ------------------------------------------------------------------------------ # Systemd Timer Schedule (User Scope) # ------------------------------------------------------------------------------ diff --git a/config/templates/default_mail_success.md b/config/templates/default_mail_success.md index 7e9841e4..c2e0ff5e 100644 --- a/config/templates/default_mail_success.md +++ b/config/templates/default_mail_success.md @@ -1,6 +1,7 @@ # ✅ Update Report: Success -The `flatpak-automatic` agent has successfully installed **${UPDATE_COUNT}** +The `flatpak-automatic` agent has successfully installed +**${UPDATE_COUNT}** updates on the host **${HOSTNAME}**. ## Installation Details diff --git a/docs/development.md b/docs/development.md index 163291df..003466ea 100644 --- a/docs/development.md +++ b/docs/development.md @@ -75,6 +75,20 @@ Usage: [--dry-run] ``` +## Screenshot Generation Tool + +A `scripts/maintainer/generate-screenshot.py` script automatically captures the +output of `flatpak-automatic -s` (including the full ASCII header banner) and +renders a high-resolution terminal screenshot image saved at +`assets/status-screenshot.png` for display in `README.md`. This script is +automatically invoked during release version bumps via `tbump`. + +Usage: + +```bash +python3 scripts/maintainer/generate-screenshot.py +``` + ## Testing Matrix This project enforces a Shift-Left testing approach. diff --git a/docs/flatpak-automatic.1 b/docs/flatpak-automatic.1 index d7e69698..a119e7e4 100644 --- a/docs/flatpak-automatic.1 +++ b/docs/flatpak-automatic.1 @@ -4,7 +4,7 @@ flatpak-automatic \- Secure, systemd-native automation for Flatpak updates .SH SYNOPSIS .B flatpak-automatic -[\fB\-d\fR] [\fB\-t\fR] [\fB\-f\fR] [\fB\-s\fR] [\fB\-l\fR] [\fB\-a\fR] [\fB\-c\fR] [\fB\-r\fR] [\fB\-e\fR] [\fB\-x\fR] [\fB\-\-desktop\-mode\fR] +[\fB\-d\fR] [\fB\-t\fR] [\fB\-f\fR] [\fB\-s\fR] [\fB\-B\fR] [\fB\-v\fR] [\fB\-V\fR] [\fB\-l\fR] [\fB\-a\fR] [\fB\-c\fR] [\fB\-r\fR] [\fB\-e\fR] [\fB\-x\fR] [\fB\-\-desktop\-mode\fR] .SH DESCRIPTION .B flatpak-automatic is a secure, systemd-native automation wrapper for Flatpak updates. It features Snapper-integrated atomic rollbacks, multi-channel alerting (Apprise, Mail, Webhooks, Desktop), and supports both system-wide and rootless user-level execution. Designed for reliability and ease of use, it ensures your Flatpak environment remains current and resilient. @@ -32,6 +32,15 @@ Force the update process, bypassing the minimum delay guard and auto_update poli .BR \-s ", " \-\-status Display a system monitoring overview of configuration and pending updates. .TP +.BR \-B ", " \-\-hide\-banner +Hide the header ASCII banner upon execution. +.TP +.BR \-v ", " \-\-version +Display the installed application version and exit. +.TP +.BR \-V ", " \-\-verbose +Enable verbose output (e.g. detailed status view of installed Flatpaks and runtimes with -s/--status). +.TP .BR \-l ", " \-\-history Display recent execution history from journalctl. .TP diff --git a/scripts/maintainer/generate-screenshot.py b/scripts/maintainer/generate-screenshot.py new file mode 100755 index 00000000..94035ba3 --- /dev/null +++ b/scripts/maintainer/generate-screenshot.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Script to generate terminal status screenshot for README using rich, chrome/cairosvg.""" + +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +from PIL import Image, ImageChops +from rich.console import Console +from rich.terminal_theme import TerminalTheme +from rich.text import Text + + +# Authentic terminal color palette matching standard terminal emulators (with rich purple HEADER) +VIBRANT_TERMINAL_THEME = TerminalTheme( + background=(41, 41, 41), + foreground=(248, 248, 242), + normal=[ + (0, 0, 0), + (255, 85, 85), + (80, 250, 123), + (241, 250, 140), + (139, 233, 253), + (189, 147, 249), # Purple (#BD93F9) + (0, 229, 255), + (191, 191, 191), + ], + bright=[ + (98, 114, 164), + (255, 110, 110), + (105, 255, 148), + (255, 255, 165), + (120, 190, 255), + (189, 147, 249), # Bright Purple (#BD93F9) + (0, 229, 255), + (255, 255, 255), + ], +) + + +def crop_margins(image_path: Path) -> None: + """Crop white outer margins from generated screenshot.""" + im = Image.open(image_path) + im_rgb = im.convert("RGB") + bg = Image.new("RGB", im_rgb.size, (255, 255, 255)) + diff = ImageChops.difference(im_rgb, bg) + bbox = diff.getbbox() + if bbox: + cropped = im.crop(bbox) + cropped.save(image_path) + + +def convert_svg_to_png(svg_path: Path, output_png: Path) -> None: + """Convert SVG to PNG using Google Chrome/Chromium if available, falling back to cairosvg.""" + chrome_bin = shutil.which("google-chrome") or shutil.which("chromium") + + if chrome_bin: + temp_png = svg_path.parent / "temp_chrome.png" + cmd = [ + chrome_bin, + "--headless", + "--disable-gpu", + "--hide-scrollbars", + f"--screenshot={temp_png}", + "--window-size=1000,800", + str(svg_path), + ] + subprocess.run( + cmd, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + if temp_png.exists(): + crop_margins(temp_png) + shutil.move(temp_png, output_png) + return + + # Fallback to cairosvg + import cairosvg + + svg_data = svg_path.read_text(encoding="utf-8") + svg_data = re.sub( + r"font-family:\s*([^;]+);", + r"font-family: \1, 'Noto Color Emoji', 'Noto Emoji', 'Segoe UI Emoji', 'Apple Color Emoji', 'Symbola', monospace;", + svg_data, + ) + cairosvg.svg2png( + bytestring=svg_data.encode("utf-8"), + write_to=str(output_png), + ) + + +def generate_screenshot() -> None: + """Run status command as root (or fallback), capture terminal output with ASCII header banner, and export to PNG image.""" + script_path = Path(__file__).resolve() + repo_root = script_path.parent.parent.parent + assets_dir = repo_root / "assets" + assets_dir.mkdir(parents=True, exist_ok=True) + output_png = assets_dir / "status-screenshot.png" + temp_svg = assets_dir / "temp_status.svg" + + src_dir = str(repo_root / "src") + if src_dir not in sys.path: + sys.path.insert(0, src_dir) + + from flatpak_automatic.cli import banner + + cli_script = repo_root / "src" / "flatpak-automatic.py" + + output_text = None + + # Try non-interactive sudo first, then interactive sudo, then non-root execution + attempts = [ + ["sudo", "-n", sys.executable, str(cli_script), "-s"], + ["sudo", sys.executable, str(cli_script), "-s"], + [sys.executable, str(cli_script), "-s"], + ] + + if os.geteuid() == 0: + attempts.insert(0, [sys.executable, str(cli_script), "-s"]) + + for cmd in attempts: + try: + result = subprocess.run( + cmd, + cwd=repo_root, + capture_output=True, + text=True, + check=True, + timeout=5, + ) + output_text = result.stdout.strip() + if output_text: + break + except Exception: + continue + + if not output_text: + # Ultimate fallback + result = subprocess.run( + [sys.executable, str(cli_script), "-s"], + cwd=repo_root, + capture_output=True, + text=True, + check=True, + ) + output_text = result.stdout.strip() + + banner_text = banner() + full_output = f"{banner_text}\n{output_text}" + + console = Console(record=True, width=100) + rich_text = Text.from_ansi(full_output) + console.print(rich_text) + + svg_data = console.export_svg( + title="flatpak-automatic -s", theme=VIBRANT_TERMINAL_THEME + ) + temp_svg.write_text(svg_data, encoding="utf-8") + + convert_svg_to_png(temp_svg, output_png) + + if temp_svg.exists(): + temp_svg.unlink() + + print(f"Successfully generated screenshot at: {output_png}") + + +if __name__ == "__main__": + generate_screenshot() diff --git a/src/flatpak_automatic/__main__.py b/src/flatpak_automatic/__main__.py index b0dc36f8..f9aa188b 100644 --- a/src/flatpak_automatic/__main__.py +++ b/src/flatpak_automatic/__main__.py @@ -4,6 +4,7 @@ import signal import subprocess from typing import Any +from . import __version__ from .cli import get_parser, banner from .config import ConfigManager, StateManager from .core import AutomationEngine @@ -15,18 +16,26 @@ def main() -> None: # 1. Initialize Parser & Logging parser = get_parser() args = parser.parse_args() + + if args.version: + print(f"flatpak-automatic {__version__}") + sys.exit(0) + setup_logging() # Dynamic Flatpak Scope for non-root execution user_scope = os.geteuid() != 0 flatpak_scope = ["--user"] if user_scope else ["--system"] - if sys.stdout.isatty(): - print(banner()) - # 2. Load Configuration & State config = ConfigManager.load() + # Determine if banner should be displayed + hide_banner = args.hide_banner or config.get("cli", {}).get("hide_banner", False) + + if sys.stdout.isatty() and not hide_banner: + print(banner()) + def sighup_handler(signum: int, frame: Any) -> None: logging.info("SIGHUP received. Hot-reloading configuration...") nonlocal config @@ -160,8 +169,8 @@ def sighup_handler(signum: int, frame: Any) -> None: print() sys.exit(0) - if args.status: - engine.print_status_overview() + if args.status or args.verbose: + engine.print_status_overview(verbose=args.verbose) sys.exit(0) if args.history: diff --git a/src/flatpak_automatic/cli.py b/src/flatpak_automatic/cli.py index 2fa93b58..b71af3ff 100644 --- a/src/flatpak_automatic/cli.py +++ b/src/flatpak_automatic/cli.py @@ -51,6 +51,24 @@ def get_parser() -> BrandedArgumentParser: action="store_true", help="Display system monitoring overview and exit.", ) + parser.add_argument( + "-B", + "--hide-banner", + action="store_true", + help="Hide the header ASCII banner upon execution.", + ) + parser.add_argument( + "-v", + "--version", + action="store_true", + help="Show application version and exit.", + ) + parser.add_argument( + "-V", + "--verbose", + action="store_true", + help="Enable verbose output (e.g. detailed status view with -s/--status).", + ) parser.add_argument( "-l", "--history", diff --git a/src/flatpak_automatic/core.py b/src/flatpak_automatic/core.py index 5185e7e7..8d516a44 100644 --- a/src/flatpak_automatic/core.py +++ b/src/flatpak_automatic/core.py @@ -91,7 +91,7 @@ def dispatch_test_notifications(self) -> None: update_count=0, ) - def print_status_overview(self) -> None: + def print_status_overview(self, verbose: bool = False) -> None: from .notifiers import APPRISE_AVAILABLE, MailNotifier print( @@ -209,23 +209,74 @@ def print_status_overview(self) -> None: webhook_icon = "🟢" if webhook_avail else "⚪" print(f" Network/Webhook: {webhook_icon} {webhook_status}") - print(f"\n{Colors.OKCYAN}📦 Installed Flatpaks:{Colors.ENDC}") - result = subprocess.run( - ["flatpak", "list", "--app", "--columns=application,version"], - capture_output=True, - text=True, - ) exclusions = self.config.get("exclusions", []) - for line in result.stdout.strip().split("\n"): - if not line: - continue - - # Simple parsing of application ID (first column) - app_id = line.split()[0] - if app_id in exclusions: - print(f" {Colors.WARNING}{line} (Excluded){Colors.ENDC}") - else: - print(f" {line}") + + if verbose: + print( + f"\n{Colors.OKCYAN}📦 Installed Flatpaks & Runtimes (Verbose):{Colors.ENDC}" + ) + result = subprocess.run( + [ + "flatpak", + "list", + "--columns=name,application,version,branch,installation", + ], + capture_output=True, + text=True, + ) + raw_lines = [ + line.split("\t") + for line in result.stdout.strip().split("\n") + if line.strip() + ] + + if raw_lines: + headers = [ + "NAME", + "APPLICATION ID", + "VERSION", + "BRANCH", + "INSTALLATION", + ] + col_widths = [len(h) for h in headers] + for row in raw_lines: + for i in range(min(5, len(row))): + col_widths[i] = max(col_widths[i], len(row[i])) + + header_str = " " + " ".join( + f"{headers[i]:<{col_widths[i]}}" for i in range(5) + ) + print(f"{Colors.BOLD}{Colors.OKCYAN}{header_str}{Colors.ENDC}") + + for row in raw_lines: + padded_row = row + [""] * (5 - len(row)) + app_id = padded_row[1] + formatted_row = " " + " ".join( + f"{padded_row[i]:<{col_widths[i]}}" for i in range(5) + ) + + if app_id in exclusions: + print( + f" {Colors.WARNING}{formatted_row.strip()} (Excluded){Colors.ENDC}" + ) + else: + print(f"{formatted_row}") + else: + print(f"\n{Colors.OKCYAN}📦 Installed Flatpaks:{Colors.ENDC}") + result = subprocess.run( + ["flatpak", "list", "--app", "--columns=application,version"], + capture_output=True, + text=True, + ) + for line in result.stdout.strip().split("\n"): + if not line: + continue + + app_id = line.split()[0] + if app_id in exclusions: + print(f" {Colors.WARNING}{line} (Excluded){Colors.ENDC}") + else: + print(f" {line}") print() # Trailing empty line def run( diff --git a/tbump.toml b/tbump.toml index 939887f0..53797280 100644 --- a/tbump.toml +++ b/tbump.toml @@ -24,6 +24,10 @@ tag_template = "v{new_version}" name = "Generate Changelogs and Package Metadata" cmd = "python3 scripts/build/update-package-metadata.py --version {new_version} --generate-changelog" +[[before_commit]] +name = "Generate Terminal Status Screenshot" +cmd = "python3 scripts/maintainer/generate-screenshot.py" + # Pre-run hooks to ensure everything is formatted before tbump tries to commit. # This prevents failures where hooks modify files during the git commit step. [[before_commit]] diff --git a/tests/test_core_status.py b/tests/test_core_status.py index 5c53d0a6..0df13e0f 100644 --- a/tests/test_core_status.py +++ b/tests/test_core_status.py @@ -64,3 +64,38 @@ def test_print_status_timer_disabled(self, mock_run: MagicMock) -> None: call.args[0] if call.args else "" for call in mock_print.call_args_list ] assert any("Disabled / Inactive" in arg for arg in printed_args) + + @patch("subprocess.run") + @patch("flatpak_automatic.notifiers.MailNotifier.is_available") + @patch("flatpak_automatic.notifiers.DesktopNotifier.is_available") + @patch("flatpak_automatic.notifiers.WebhookNotifier.is_available") + def test_print_status_overview_verbose( + self, + mock_wh: MagicMock, + mock_dt: MagicMock, + mock_mail: MagicMock, + mock_run: MagicMock, + ) -> None: + mock_run.side_effect = [ + MagicMock(returncode=1), # is-enabled + MagicMock(returncode=1), # is-active + MagicMock( + stdout="App\torg.test.App\t1.0\tstable\tsystem\n", returncode=0 + ), # flatpak list verbose + ] + mock_mail.return_value = True + mock_dt.return_value = True + mock_wh.return_value = True + + engine = AutomationEngine({"exclusions": ["org.test.App"]}, {}) + with patch("builtins.print") as mock_print: + engine.print_status_overview(verbose=True) + printed_args = [ + call.args[0] if call.args else "" for call in mock_print.call_args_list + ] + assert any( + "📦 Installed Flatpaks & Runtimes (Verbose):" in arg + for arg in printed_args + ) + assert any("APPLICATION ID" in arg for arg in printed_args) + assert any("(Excluded)" in arg for arg in printed_args) diff --git a/tests/test_main_cli.py b/tests/test_main_cli.py index ff66bf7c..5b99c813 100644 --- a/tests/test_main_cli.py +++ b/tests/test_main_cli.py @@ -15,7 +15,7 @@ def test_main_check_config( mock_config: MagicMock, mock_parser: MagicMock, ) -> None: - args = MagicMock() + args = MagicMock(version=False) args.check_config = True args.reload = False args.apply_schedule = False @@ -49,11 +49,13 @@ def test_main_status( ) -> None: args = MagicMock( check_config=False, + version=False, reload=False, apply_schedule=False, enable_timer=False, disable_timer=False, status=True, + verbose=False, history=False, test_notify=False, ) @@ -79,11 +81,13 @@ def test_main_enable_timer( ) -> None: args = MagicMock( check_config=False, + version=False, reload=False, apply_schedule=False, enable_timer=True, disable_timer=False, status=False, + verbose=False, history=False, test_notify=False, ) @@ -110,11 +114,13 @@ def test_main_disable_timer( ) -> None: args = MagicMock( check_config=False, + version=False, reload=False, apply_schedule=False, enable_timer=False, disable_timer=True, status=False, + verbose=False, history=False, test_notify=False, ) @@ -141,11 +147,13 @@ def test_main_reload( ) -> None: args = MagicMock( check_config=False, + version=False, reload=True, apply_schedule=False, enable_timer=False, disable_timer=False, status=False, + verbose=False, history=False, test_notify=False, ) @@ -173,11 +181,13 @@ def test_main_history( ) -> None: args = MagicMock( check_config=False, + version=False, reload=False, apply_schedule=False, enable_timer=False, disable_timer=False, status=False, + verbose=False, history=True, test_notify=False, ) @@ -202,11 +212,13 @@ def test_main_test_notify( ) -> None: args = MagicMock( check_config=False, + version=False, reload=False, apply_schedule=False, enable_timer=False, disable_timer=False, status=False, + verbose=False, history=False, test_notify=True, ) @@ -217,6 +229,92 @@ def test_main_test_notify( assert e.value.code == 0 mock_engine.return_value.dispatch_test_notifications.assert_called_once() + @patch("flatpak_automatic.__main__.get_parser") + @patch("flatpak_automatic.__main__.ConfigManager.load") + @patch("flatpak_automatic.__main__.StateManager.load") + @patch("flatpak_automatic.__main__.AutomationEngine") + @patch("sys.stdout.isatty", return_value=True) + @patch("flatpak_automatic.__main__.banner") + def test_main_hide_banner_cli( + self, + mock_banner: MagicMock, + mock_isatty: MagicMock, + mock_engine: MagicMock, + mock_state: MagicMock, + mock_config: MagicMock, + mock_parser: MagicMock, + ) -> None: + args = MagicMock( + check_config=True, + hide_banner=True, + reload=False, + apply_schedule=False, + enable_timer=False, + disable_timer=False, + status=False, + verbose=False, + history=False, + test_notify=False, + ) + mock_parser.return_value.parse_args.return_value = args + mock_config.return_value = {"cli": {"hide_banner": False}} + + with pytest.raises(SystemExit) as e: + main() + assert e.value.code == 0 + mock_banner.assert_not_called() + + @patch("flatpak_automatic.__main__.get_parser") + @patch("flatpak_automatic.__main__.ConfigManager.load") + @patch("flatpak_automatic.__main__.StateManager.load") + @patch("flatpak_automatic.__main__.AutomationEngine") + @patch("sys.stdout.isatty", return_value=True) + @patch("flatpak_automatic.__main__.banner") + def test_main_hide_banner_config( + self, + mock_banner: MagicMock, + mock_isatty: MagicMock, + mock_engine: MagicMock, + mock_state: MagicMock, + mock_config: MagicMock, + mock_parser: MagicMock, + ) -> None: + args = MagicMock( + check_config=True, + hide_banner=False, + reload=False, + apply_schedule=False, + enable_timer=False, + disable_timer=False, + status=False, + verbose=False, + history=False, + test_notify=False, + ) + mock_parser.return_value.parse_args.return_value = args + mock_config.return_value = {"cli": {"hide_banner": True}} + + with pytest.raises(SystemExit) as e: + main() + assert e.value.code == 0 + mock_banner.assert_not_called() + + @patch("flatpak_automatic.__main__.get_parser") + @patch("builtins.print") + def test_main_version( + self, + mock_print: MagicMock, + mock_parser: MagicMock, + ) -> None: + args = MagicMock(version=True) + mock_parser.return_value.parse_args.return_value = args + + with pytest.raises(SystemExit) as e: + main() + assert e.value.code == 0 + mock_print.assert_called_once() + assert "flatpak-automatic" in mock_print.call_args[0][0] + @patch("flatpak_automatic.__main__.get_parser") @patch("flatpak_automatic.__main__.ConfigManager.load") @patch("flatpak_automatic.__main__.StateManager.load") @@ -234,11 +332,14 @@ def test_main_apply_schedule( ) -> None: args = MagicMock( check_config=False, + version=False, + hide_banner=False, reload=False, apply_schedule=True, enable_timer=False, disable_timer=False, status=False, + verbose=False, history=False, test_notify=False, )