usage-cli 0.34.14__tar.gz → 0.34.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {usage_cli-0.34.14/usage_cli.egg-info → usage_cli-0.34.15}/PKG-INFO +1 -1
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/agy_quota_probe.py +20 -2
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/agy.py +5 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/pyproject.toml +1 -1
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/agy_window_keeper.py +1 -1
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_agy_quota_probe.py +68 -1
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_agy_window_keeper.py +35 -1
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_keeper_outcome.py +4 -2
- usage_cli-0.34.15/tests/test_network.py +97 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_cli.py +2 -1
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_wintray.py +76 -2
- {usage_cli-0.34.14 → usage_cli-0.34.15/usage_cli.egg-info}/PKG-INFO +1 -1
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/network.py +25 -3
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/app.py +14 -5
- usage_cli-0.34.14/tests/test_network.py +0 -41
- {usage_cli-0.34.14 → usage_cli-0.34.15}/LICENSE +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/README.md +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/THIRD_PARTY_NOTICES.md +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/adapters/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/adapters/agy.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/adapters/claude.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/adapters/codex.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/adapters/grok.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/adapters/muse.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/adapters/rate_limits.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/adapters/registry.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/adapters/types.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/aggregator.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/blocks.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/history_persist.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/insights.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/ledger_backfill.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/ledger_legacy_backfill.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/reporter.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/subscription.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/i18n.json +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/installer/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/installer/claude_pane.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/installer/codex_hook_trust.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/installer/login_item.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/installer/session_hooks.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/installer/setup_hook.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/installer/statusline_settings.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/_shared_cache_file.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/agy_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/chromium_cache.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/claude_desktop.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/claude_paths.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/claude_usage.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/codex_events.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/codex_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/codex_paths.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/grok_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/history_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/muse_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/loaders/muse_quota_probe.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/main.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/actions.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/app.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/chrome.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/fsevents_watch.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/grok.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/manual_refresh.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/menu.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/muse.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/new_badge.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/notify.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/popover.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/prefs.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/quota_countdown.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/refresh.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/state.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/title.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/update.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/menubar/whats_new.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/base.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/catalog.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/dynamic_height.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/panel_scale.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/panel_window.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/panel_window_state.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/payload.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/web_panel.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/panels/window_drag.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/burn_rate.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/keeper_outcome.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/notifications.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/quota_snapshot.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/status_payload.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/usage_rate.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/quota/window_keeper.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/setup.cfg +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/setup_app.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_aggregator.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_agy_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_app_version.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_check_release_version.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_check_upstream.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_chromium_cache.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_claude_desktop.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_claude_expired_windows.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_claude_pane.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_cli_lookup.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_codex_hook_trust.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_compact_cards.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_desktop_fresh_cache.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_desktop_quota_display.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_desktop_quota_review.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_doctor.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_history_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_history_persist.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_html_report_snapshot.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_insights.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_keeper_menu.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_ledger_backfill.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_ledger_legacy_backfill.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_login_item.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_main.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_manual_refresh.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_menubar.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_menubar_agy.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_menubar_muse.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_menubar_permission.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_menubar_state.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_menubar_update.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_muse_coverage.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_muse_quota_probe.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_muse_review_regressions.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_new_badge.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_packaging.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_panel_footer_layout.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_panels.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_pricing.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_quota_aware.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_quota_label.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_quota_threshold_menu.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_report_chart_redesign.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_report_cold_read.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_report_filter_js.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_report_headings.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_report_share_mask.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_service_status.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_session_resume.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_setup_app.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_subscription.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_taskbar_menu.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_taskbar_obstacles.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_time_utils.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_tui.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_update_checker.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_update_gate.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_client.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_web_panel_payload.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_whats_new.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tui/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tui/app.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/tui/sprite.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/ui/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/ui/html_report.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/ui/report_charts.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/ui/report_filter.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/ui/report_ledger.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/ui/report_scripts.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/ui/report_styles.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/ui/tables.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/updates/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/updates/checker.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/updates/gate.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/updates/release_notes.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/updates/whats_new.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_app/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_app/cli.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_app/doctor.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_cli.egg-info/SOURCES.txt +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/app_version.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/cli_lookup.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/i18n.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/prefs.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/pricing.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/project_resolver.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/service_status.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/time_utils.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_hooks/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_hooks/usage_quota_aware.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_hooks/usage_session_resume.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_hooks/usage_statusline.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_hooks/usage_statusline_agy.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_hooks/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_hooks/usage_statusline_grok.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_hooks/usage_terse_mode.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/usage_hooks/usage_terse_reminder.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/__init__.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/login_item.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/menu.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/prefs.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/quota_label.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/taskbar_menu.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/taskbar_obstacles.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/taskbar_overlay.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/watch.py +0 -0
- {usage_cli-0.34.14 → usage_cli-0.34.15}/wintray/whats_new.py +0 -0
|
@@ -34,6 +34,7 @@ from typing import Any
|
|
|
34
34
|
from urllib.error import HTTPError, URLError
|
|
35
35
|
from urllib.request import Request, urlopen
|
|
36
36
|
|
|
37
|
+
from loaders.cache_quarantine import quarantine
|
|
37
38
|
from usage_common.cli_lookup import cli_env, resolve_cli
|
|
38
39
|
from usage_common.network import google_auth_reachable
|
|
39
40
|
from usage_common.subprocess_utils import hidden_console_kwargs
|
|
@@ -86,6 +87,9 @@ _cli_retry_after_monotonic = 0.0
|
|
|
86
87
|
# When each group last appeared in a real API response; in memory only, reset on restart.
|
|
87
88
|
_group_seen_at: dict[str, datetime] = {}
|
|
88
89
|
_MISSING_GROUP_GRACE_MINUTES = 30
|
|
90
|
+
# Models omit the period; keep known weekly resets weekly near expiry.
|
|
91
|
+
# In memory only: after restart, unseen resets use the time heuristic again.
|
|
92
|
+
_weekly_reset_times: set[str] = set()
|
|
89
93
|
|
|
90
94
|
|
|
91
95
|
def _user_agent() -> str:
|
|
@@ -527,6 +531,7 @@ def _post_json(url: str, access_token: str, body: dict[str, object], timeout: fl
|
|
|
527
531
|
except HTTPError as exc:
|
|
528
532
|
_handle_http_error(exc)
|
|
529
533
|
if exc.code == 401:
|
|
534
|
+
_token_cache.clear()
|
|
530
535
|
_mark_auth_expired()
|
|
531
536
|
return None
|
|
532
537
|
except (URLError, OSError, ValueError):
|
|
@@ -637,7 +642,13 @@ def _gemini_group_from_models(raw: object) -> AgyQuotaGroup | None:
|
|
|
637
642
|
window = _bucket_to_window(bucket)
|
|
638
643
|
reset = _bucket_reset_time(bucket)
|
|
639
644
|
assert reset is not None
|
|
640
|
-
|
|
645
|
+
_weekly_reset_times.difference_update(
|
|
646
|
+
[seen for seen in _weekly_reset_times if _seconds_until(seen) <= 0]
|
|
647
|
+
)
|
|
648
|
+
session = reset not in _weekly_reset_times and _seconds_until(reset) <= 5 * 3600 + 5 * 60
|
|
649
|
+
# A full bucket's reset time slides with every response, so only a spent one is remembered.
|
|
650
|
+
if not session and window is not None and window.remaining_percent < 100.0:
|
|
651
|
+
_weekly_reset_times.add(reset)
|
|
641
652
|
template = _assemble_groups({"gemini": {}})[0]
|
|
642
653
|
return AgyQuotaGroup(
|
|
643
654
|
name=template.name,
|
|
@@ -825,7 +836,14 @@ def _read_cache() -> AgyQuotaResult | None:
|
|
|
825
836
|
try:
|
|
826
837
|
with CACHE_PATH.open(encoding="utf-8") as cache_file:
|
|
827
838
|
payload: object = json.load(cache_file)
|
|
828
|
-
except (
|
|
839
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
840
|
+
quarantine(
|
|
841
|
+
CACHE_PATH, "decode-error" if isinstance(exc, UnicodeDecodeError) else "json-error"
|
|
842
|
+
)
|
|
843
|
+
with suppress(OSError):
|
|
844
|
+
CACHE_PATH.unlink()
|
|
845
|
+
return None
|
|
846
|
+
except OSError:
|
|
829
847
|
return None
|
|
830
848
|
return _result_from_payload(payload)
|
|
831
849
|
|
|
@@ -59,6 +59,7 @@ class AgyQuotaProjection:
|
|
|
59
59
|
five_hour: AgyQuotaWindow | None
|
|
60
60
|
group_switchable: bool = False
|
|
61
61
|
weekly_only: bool = False
|
|
62
|
+
gemini_five_hour: AgyQuotaWindow | None = None
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
@dataclass(frozen=True, slots=True)
|
|
@@ -162,6 +163,10 @@ def project_quota(
|
|
|
162
163
|
weekly=weekly,
|
|
163
164
|
stale=stale,
|
|
164
165
|
five_hour=selected.five_hour,
|
|
166
|
+
gemini_five_hour=next(
|
|
167
|
+
(group.five_hour for group in quota.groups if _matches_group(group.name, "gemini")),
|
|
168
|
+
None,
|
|
169
|
+
),
|
|
165
170
|
)
|
|
166
171
|
|
|
167
172
|
|
|
@@ -118,7 +118,7 @@ def maybe_ping(result: AgyRefreshResult, mock: bool) -> None:
|
|
|
118
118
|
if not enabled:
|
|
119
119
|
return
|
|
120
120
|
projection = result.projection
|
|
121
|
-
five_hour = projection.
|
|
121
|
+
five_hour = projection.gemini_five_hour if projection is not None else None
|
|
122
122
|
now = time.time()
|
|
123
123
|
last_ping_at = _load_last_ping()
|
|
124
124
|
state = keeper_outcome.read_state(AGY_WINDOW_KEEPER_STATE_PATH)
|
|
@@ -21,7 +21,7 @@ from urllib.request import Request
|
|
|
21
21
|
|
|
22
22
|
import pytest
|
|
23
23
|
|
|
24
|
-
from loaders import agy_quota_probe
|
|
24
|
+
from loaders import agy_quota_probe, cache_quarantine
|
|
25
25
|
from loaders.agy_quota_probe import (
|
|
26
26
|
AgyQuotaGroup,
|
|
27
27
|
AgyQuotaResult,
|
|
@@ -111,12 +111,14 @@ def _reset_token_cache(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
|
|
111
111
|
agy_quota_probe._rate_limit_until_monotonic = 0.0
|
|
112
112
|
agy_quota_probe._cli_retry_after_monotonic = 0.0
|
|
113
113
|
agy_quota_probe._group_seen_at.clear()
|
|
114
|
+
agy_quota_probe._weekly_reset_times.clear()
|
|
114
115
|
monkeypatch.setattr(agy_quota_probe, "_read_macos_credential", lambda: None)
|
|
115
116
|
yield
|
|
116
117
|
agy_quota_probe._token_cache.clear()
|
|
117
118
|
agy_quota_probe._rate_limit_until_monotonic = 0.0
|
|
118
119
|
agy_quota_probe._cli_retry_after_monotonic = 0.0
|
|
119
120
|
agy_quota_probe._group_seen_at.clear()
|
|
121
|
+
agy_quota_probe._weekly_reset_times.clear()
|
|
120
122
|
|
|
121
123
|
|
|
122
124
|
# --- find_agy (unchanged behavior) -----------------------------------------
|
|
@@ -683,6 +685,36 @@ def test_load_quota_returns_fresh_cache_without_probing(
|
|
|
683
685
|
assert agy_quota_probe.load_quota() == cached
|
|
684
686
|
|
|
685
687
|
|
|
688
|
+
@pytest.mark.parametrize(
|
|
689
|
+
"contents,reason", [(b"\xff", "decode-error"), (b"{", "json-error"), (None, None)]
|
|
690
|
+
)
|
|
691
|
+
def test_load_quota_probes_after_corrupt_or_missing_cache(
|
|
692
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, contents: bytes | None, reason: str | None
|
|
693
|
+
) -> None:
|
|
694
|
+
cache_path = tmp_path / "agy_quota_cache.json"
|
|
695
|
+
quarantine_dir = tmp_path / "quarantine"
|
|
696
|
+
monkeypatch.setattr(agy_quota_probe, "CACHE_PATH", cache_path)
|
|
697
|
+
monkeypatch.setattr(cache_quarantine, "QUARANTINE_DIR", quarantine_dir)
|
|
698
|
+
if contents is not None:
|
|
699
|
+
cache_path.write_bytes(contents)
|
|
700
|
+
calls: list[tuple[Path, str]] = []
|
|
701
|
+
quarantine = cache_quarantine.quarantine
|
|
702
|
+
|
|
703
|
+
def record_quarantine(path: Path, why: str) -> None:
|
|
704
|
+
calls.append((path, why))
|
|
705
|
+
quarantine(path, why)
|
|
706
|
+
|
|
707
|
+
monkeypatch.setattr(agy_quota_probe, "quarantine", record_quarantine)
|
|
708
|
+
fresh = _result(datetime.now(UTC))
|
|
709
|
+
monkeypatch.setattr(agy_quota_probe, "probe_quota", lambda: fresh)
|
|
710
|
+
|
|
711
|
+
assert agy_quota_probe.load_quota() == fresh
|
|
712
|
+
assert calls == ([(cache_path, reason)] if reason is not None else [])
|
|
713
|
+
backups = list(quarantine_dir.glob("*.bak"))
|
|
714
|
+
assert [backup.read_bytes() for backup in backups] == ([contents] if contents else [])
|
|
715
|
+
assert agy_quota_probe._read_cache() == fresh
|
|
716
|
+
|
|
717
|
+
|
|
686
718
|
def test_load_quota_probes_when_cache_is_stale(
|
|
687
719
|
monkeypatch: pytest.MonkeyPatch,
|
|
688
720
|
tmp_path: Path,
|
|
@@ -748,6 +780,24 @@ def test_auth_expiration_marker(
|
|
|
748
780
|
assert agy_quota_probe.auth_expired() is expired
|
|
749
781
|
|
|
750
782
|
|
|
783
|
+
def test_quota_401_clears_cached_token(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
784
|
+
token_path = tmp_path / "antigravity-oauth-token"
|
|
785
|
+
_write_token(token_path, access_token="new-token")
|
|
786
|
+
monkeypatch.setattr(agy_quota_probe, "_TOKEN_PATH", token_path)
|
|
787
|
+
agy_quota_probe._token_cache.update(
|
|
788
|
+
access_token="rejected-token", expires_monotonic=time.monotonic() + 3600
|
|
789
|
+
)
|
|
790
|
+
urlopen, visited = _build_http_error_urlopen(401)
|
|
791
|
+
monkeypatch.setattr(agy_quota_probe, "urlopen", urlopen)
|
|
792
|
+
|
|
793
|
+
token = agy_quota_probe._resolve_access_token(1)
|
|
794
|
+
assert token == "rejected-token"
|
|
795
|
+
assert agy_quota_probe._post_json(_QUOTA_URL, token, {}, 1) is None
|
|
796
|
+
assert visited == [(_QUOTA_URL, "Bearer rejected-token")]
|
|
797
|
+
assert agy_quota_probe._token_cache == {}
|
|
798
|
+
assert agy_quota_probe._resolve_access_token(1) == "new-token"
|
|
799
|
+
|
|
800
|
+
|
|
751
801
|
def test_successful_probe_clears_expiration_marker(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
752
802
|
result = AgyQuotaResult(groups=[], fetched_at=datetime.now(UTC).isoformat())
|
|
753
803
|
agy_quota_probe.AUTH_EXPIRED_PATH.touch()
|
|
@@ -916,6 +966,23 @@ def test_probe_models_fallback_selects_lowest_gemini(
|
|
|
916
966
|
]
|
|
917
967
|
|
|
918
968
|
|
|
969
|
+
def test_models_fallback_keeps_weekly_reset_near_expiry(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
970
|
+
reset = (datetime.now(UTC) + timedelta(hours=6)).isoformat()
|
|
971
|
+
raw = {
|
|
972
|
+
"models": {"gemini-flash": {"quotaInfo": {"remainingFraction": 0.5, "resetTime": reset}}}
|
|
973
|
+
}
|
|
974
|
+
seconds_left = 6 * 3600
|
|
975
|
+
monkeypatch.setattr(agy_quota_probe, "_seconds_until", lambda _: seconds_left)
|
|
976
|
+
|
|
977
|
+
first = agy_quota_probe._gemini_group_from_models(raw)
|
|
978
|
+
assert first is not None and first.weekly is not None and first.five_hour is None
|
|
979
|
+
|
|
980
|
+
seconds_left = 5 * 3600 + 5 * 60
|
|
981
|
+
refreshed = agy_quota_probe._gemini_group_from_models(raw)
|
|
982
|
+
assert refreshed is not None and refreshed.weekly is not None
|
|
983
|
+
assert refreshed.five_hour is None
|
|
984
|
+
|
|
985
|
+
|
|
919
986
|
@pytest.mark.parametrize(
|
|
920
987
|
"models",
|
|
921
988
|
[None, {"models": {}}, {"models": {"claude": {"quotaInfo": {"resetTime": _FUTURE_RESET}}}}],
|
|
@@ -8,13 +8,14 @@ import shutil
|
|
|
8
8
|
import subprocess
|
|
9
9
|
import sys
|
|
10
10
|
import threading
|
|
11
|
+
from datetime import UTC, datetime
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any, cast
|
|
13
14
|
|
|
14
15
|
import pytest
|
|
15
16
|
|
|
16
17
|
import quota.agy_window_keeper as agy_window_keeper
|
|
17
|
-
from loaders.agy_quota_probe import AgyQuotaWindow
|
|
18
|
+
from loaders.agy_quota_probe import AgyQuotaGroup, AgyQuotaResult, AgyQuotaWindow
|
|
18
19
|
from menubar import agy as menubar_agy
|
|
19
20
|
from menubar.state import AgyStaleState, QuotaRowState
|
|
20
21
|
from quota import keeper_outcome
|
|
@@ -158,6 +159,7 @@ def _refresh_result(
|
|
|
158
159
|
weekly=row,
|
|
159
160
|
stale=stale,
|
|
160
161
|
five_hour=AgyQuotaWindow(remaining, None, resets_in_minutes),
|
|
162
|
+
gemini_five_hour=AgyQuotaWindow(remaining, None, resets_in_minutes),
|
|
161
163
|
)
|
|
162
164
|
return menubar_agy.AgyRefreshResult(projection=projection, hide_agy=False)
|
|
163
165
|
|
|
@@ -197,6 +199,38 @@ def test_maybe_ping_fires_and_stamps_dispatch(
|
|
|
197
199
|
assert _SyncThread.instances[0].daemon is True
|
|
198
200
|
|
|
199
201
|
|
|
202
|
+
@pytest.mark.parametrize("preference", ["claude_gpt", "argon"])
|
|
203
|
+
@pytest.mark.parametrize("gemini_remaining", [None, 45.0, 100.0])
|
|
204
|
+
def test_maybe_ping_uses_gemini_window_regardless_of_display(
|
|
205
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
206
|
+
isolated_state: Path,
|
|
207
|
+
preference: str,
|
|
208
|
+
gemini_remaining: float | None,
|
|
209
|
+
) -> None:
|
|
210
|
+
calls = _arm_ping(monkeypatch)
|
|
211
|
+
displayed = AgyQuotaWindow(100.0 if gemini_remaining != 100.0 else 45.0, None, 120)
|
|
212
|
+
groups = [
|
|
213
|
+
AgyQuotaGroup("CLAUDE AND GPT MODELS", [], None, displayed),
|
|
214
|
+
AgyQuotaGroup("GEMINI 4 ARGON", [], displayed, None),
|
|
215
|
+
]
|
|
216
|
+
if gemini_remaining is not None:
|
|
217
|
+
groups.insert(
|
|
218
|
+
0,
|
|
219
|
+
AgyQuotaGroup("GEMINI MODELS", [], None, AgyQuotaWindow(gemini_remaining, None, 120)),
|
|
220
|
+
)
|
|
221
|
+
quota = AgyQuotaResult(groups, datetime.now(UTC).isoformat())
|
|
222
|
+
projection = menubar_agy.project_quota(quota, "en", group_preference=preference)
|
|
223
|
+
assert projection is not None
|
|
224
|
+
assert projection.group_name == groups[-1 if preference == "argon" else -2].name
|
|
225
|
+
assert projection.five_hour == (None if preference == "argon" else displayed)
|
|
226
|
+
|
|
227
|
+
agy_window_keeper.maybe_ping(
|
|
228
|
+
menubar_agy.AgyRefreshResult(projection=projection, hide_agy=False), mock=False
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
assert calls == (["/fake/agy"] if gemini_remaining == 100.0 else [])
|
|
232
|
+
|
|
233
|
+
|
|
200
234
|
def test_maybe_ping_disabled_has_no_side_effects(
|
|
201
235
|
monkeypatch: pytest.MonkeyPatch, isolated_state: Path
|
|
202
236
|
) -> None:
|
|
@@ -252,7 +252,7 @@ def test_other_keepers_retry_once_without_active_window(
|
|
|
252
252
|
else:
|
|
253
253
|
monkeypatch.setattr(module, "_agy_window_keeper_enabled", lambda: True)
|
|
254
254
|
window = SimpleNamespace(remaining_percent=100)
|
|
255
|
-
result = SimpleNamespace(projection=SimpleNamespace(
|
|
255
|
+
result = SimpleNamespace(projection=SimpleNamespace(gemini_five_hour=window, stale=None))
|
|
256
256
|
|
|
257
257
|
def dispatch() -> None:
|
|
258
258
|
module.maybe_ping(result, False)
|
|
@@ -387,7 +387,9 @@ def test_warning_across_windows(
|
|
|
387
387
|
)
|
|
388
388
|
if tool == "codex":
|
|
389
389
|
monkeypatch.setattr(module, "load_rate_limits", lambda: limits)
|
|
390
|
-
projection = SimpleNamespace(
|
|
390
|
+
projection = SimpleNamespace(
|
|
391
|
+
gemini_five_hour=SimpleNamespace(remaining_percent=100), stale=None
|
|
392
|
+
)
|
|
391
393
|
clock = [1000.0]
|
|
392
394
|
monkeypatch.setattr(time, "time", lambda: clock[0])
|
|
393
395
|
result = [subprocess.CompletedProcess([], 2, "", "login")]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from usage_common import network
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def _no_proxy(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
18
|
+
monkeypatch.setattr(urllib.request, "getproxies", dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _Connection:
|
|
22
|
+
def __enter__(self) -> _Connection:
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def __exit__(self, *exc: object) -> None:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_reachable_when_connection_opens(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
30
|
+
seen: list[tuple[tuple[str, int], float]] = []
|
|
31
|
+
|
|
32
|
+
def connect(address: tuple[str, int], timeout: float) -> _Connection:
|
|
33
|
+
seen.append((address, timeout))
|
|
34
|
+
return _Connection()
|
|
35
|
+
|
|
36
|
+
monkeypatch.setattr(socket, "create_connection", connect)
|
|
37
|
+
|
|
38
|
+
assert network.google_auth_reachable(timeout=1.5) is True
|
|
39
|
+
assert seen == [(("oauth2.googleapis.com", 443), 1.5)]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize("error", [socket.gaierror("no dns"), TimeoutError(), OSError("down")])
|
|
43
|
+
def test_unreachable_when_connection_fails(monkeypatch: pytest.MonkeyPatch, error: OSError) -> None:
|
|
44
|
+
def connect(address: tuple[str, int], timeout: float) -> _Connection:
|
|
45
|
+
raise error
|
|
46
|
+
|
|
47
|
+
monkeypatch.setattr(socket, "create_connection", connect)
|
|
48
|
+
|
|
49
|
+
assert network.google_auth_reachable() is False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_configured_proxy_counts_as_reachable(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
53
|
+
def connect(address: tuple[str, int], timeout: float) -> _Connection:
|
|
54
|
+
raise AssertionError("a proxy-only network blocks the direct probe")
|
|
55
|
+
|
|
56
|
+
monkeypatch.setattr(urllib.request, "getproxies", lambda: {"https": "http://proxy:8080"})
|
|
57
|
+
monkeypatch.setattr(socket, "create_connection", connect)
|
|
58
|
+
|
|
59
|
+
assert network.google_auth_reachable() is True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_hung_dns_returns_within_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
63
|
+
entered = threading.Event()
|
|
64
|
+
release = threading.Event()
|
|
65
|
+
finished = threading.Event()
|
|
66
|
+
|
|
67
|
+
def resolve(*args: object, **kwargs: object) -> list[object]:
|
|
68
|
+
try:
|
|
69
|
+
entered.set()
|
|
70
|
+
release.wait()
|
|
71
|
+
raise socket.gaierror("no dns")
|
|
72
|
+
finally:
|
|
73
|
+
finished.set()
|
|
74
|
+
|
|
75
|
+
monkeypatch.setattr(socket, "getaddrinfo", resolve)
|
|
76
|
+
start = time.monotonic()
|
|
77
|
+
try:
|
|
78
|
+
assert network.google_auth_reachable(timeout=0.05) is False
|
|
79
|
+
assert entered.is_set()
|
|
80
|
+
assert time.monotonic() - start < 0.5
|
|
81
|
+
finally:
|
|
82
|
+
release.set()
|
|
83
|
+
assert finished.wait(0.5)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_no_proxy_alone_keeps_the_probe(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
87
|
+
seen: list[tuple[str, int]] = []
|
|
88
|
+
|
|
89
|
+
def connect(address: tuple[str, int], timeout: float) -> _Connection:
|
|
90
|
+
seen.append(address)
|
|
91
|
+
raise OSError("down")
|
|
92
|
+
|
|
93
|
+
monkeypatch.setattr(urllib.request, "getproxies", lambda: {"no": "localhost"})
|
|
94
|
+
monkeypatch.setattr(socket, "create_connection", connect)
|
|
95
|
+
|
|
96
|
+
assert network.google_auth_reachable() is False
|
|
97
|
+
assert seen == [("oauth2.googleapis.com", 443)]
|
|
@@ -1095,8 +1095,9 @@ def test_status_payload_antigravity_cache(monkeypatch: pytest.MonkeyPatch, age:
|
|
|
1095
1095
|
def test_status_antigravity_unreadable_cache(content: str | None) -> None:
|
|
1096
1096
|
if content is not None:
|
|
1097
1097
|
agy_quota_probe.CACHE_PATH.write_bytes(content.encode("latin-1"))
|
|
1098
|
+
# A corrupt cache is quarantined and dropped, so every case reads as no data.
|
|
1098
1099
|
assert status_payload._status_antigravity(1_000_000) == status_payload._status_agent(
|
|
1099
|
-
None, 1_000_000, "
|
|
1100
|
+
None, 1_000_000, "no_data"
|
|
1100
1101
|
)
|
|
1101
1102
|
|
|
1102
1103
|
|
|
@@ -3573,6 +3573,7 @@ def test_windows_auto_check_schedule_finishes(
|
|
|
3573
3573
|
|
|
3574
3574
|
def test_windows_poll_loop_rechecks_each_hour(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
3575
3575
|
controller = wintray._WindowsTrayController(mock=True, interval=60)
|
|
3576
|
+
controller.visible = True
|
|
3576
3577
|
now = [0.0]
|
|
3577
3578
|
ticks = iter([100.0, 3699.0, 3700.0])
|
|
3578
3579
|
refreshes: list[float] = []
|
|
@@ -4113,12 +4114,85 @@ def test_hidden_poll_interval_with_quota_countdown(
|
|
|
4113
4114
|
waits: list[float] = []
|
|
4114
4115
|
|
|
4115
4116
|
def wait(interval: float) -> bool:
|
|
4117
|
+
if controller.stopping.is_set():
|
|
4118
|
+
return True
|
|
4116
4119
|
waits.append(interval)
|
|
4117
|
-
return
|
|
4120
|
+
return False
|
|
4121
|
+
|
|
4122
|
+
def refresh() -> None:
|
|
4123
|
+
controller.stopping.set()
|
|
4118
4124
|
|
|
4119
4125
|
monkeypatch.setattr(controller.stopping, "wait", wait)
|
|
4126
|
+
monkeypatch.setattr(controller, "refresh", refresh)
|
|
4127
|
+
monkeypatch.setattr(controller, "_clear_stale_update_cache", lambda: None)
|
|
4120
4128
|
monkeypatch.setattr("wintray.app.time.time", lambda: 1000.0)
|
|
4121
4129
|
controller._poll_loop()
|
|
4122
4130
|
|
|
4123
|
-
assert waits ==
|
|
4131
|
+
assert sum(waits) == expected_wait
|
|
4124
4132
|
assert controller._poll_thread is None
|
|
4133
|
+
|
|
4134
|
+
|
|
4135
|
+
def test_hidden_poll_refreshes_once_after_countdown_ends(
|
|
4136
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
4137
|
+
) -> None:
|
|
4138
|
+
controller = wintray._WindowsTrayController(mock=True, interval=60)
|
|
4139
|
+
controller.visible = False
|
|
4140
|
+
controller.tray_provider = "claude"
|
|
4141
|
+
controller.quota_label_enabled = True
|
|
4142
|
+
state = _state()
|
|
4143
|
+
full = replace(state.claude_weekly, percent=100, resets_at=11500.0)
|
|
4144
|
+
controller.latest_state = replace(state, claude_weekly=full)
|
|
4145
|
+
waits: list[float] = []
|
|
4146
|
+
|
|
4147
|
+
def wait(interval: float) -> bool:
|
|
4148
|
+
if controller.stopping.is_set():
|
|
4149
|
+
return True
|
|
4150
|
+
waits.append(interval)
|
|
4151
|
+
if len(waits) == 1:
|
|
4152
|
+
# The quota resets mid-wait, so the countdown is gone on the next wake.
|
|
4153
|
+
controller.latest_state = state
|
|
4154
|
+
return False
|
|
4155
|
+
|
|
4156
|
+
def refresh() -> None:
|
|
4157
|
+
controller.stopping.set()
|
|
4158
|
+
|
|
4159
|
+
monkeypatch.setattr(controller.stopping, "wait", wait)
|
|
4160
|
+
monkeypatch.setattr(controller, "refresh", refresh)
|
|
4161
|
+
monkeypatch.setattr(controller, "_clear_stale_update_cache", lambda: None)
|
|
4162
|
+
monkeypatch.setattr("wintray.app.time.time", lambda: 1000.0)
|
|
4163
|
+
controller._poll_loop()
|
|
4164
|
+
|
|
4165
|
+
assert waits == [60]
|
|
4166
|
+
|
|
4167
|
+
|
|
4168
|
+
def test_hidden_poll_refreshes_soon_after_countdown_starts(
|
|
4169
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
4170
|
+
) -> None:
|
|
4171
|
+
controller = wintray._WindowsTrayController(mock=True, interval=60)
|
|
4172
|
+
controller.visible = False
|
|
4173
|
+
controller.tray_provider = "claude"
|
|
4174
|
+
controller.quota_label_enabled = True
|
|
4175
|
+
state = _state()
|
|
4176
|
+
controller.latest_state = state
|
|
4177
|
+
waits: list[float] = []
|
|
4178
|
+
|
|
4179
|
+
def wait(interval: float) -> bool:
|
|
4180
|
+
if controller.stopping.is_set():
|
|
4181
|
+
return True
|
|
4182
|
+
waits.append(interval)
|
|
4183
|
+
if len(waits) == 1:
|
|
4184
|
+
# A file event fills the weekly quota while the hidden loop is waiting.
|
|
4185
|
+
full = replace(state.claude_weekly, percent=100, resets_at=11500.0)
|
|
4186
|
+
controller.latest_state = replace(state, claude_weekly=full)
|
|
4187
|
+
return False
|
|
4188
|
+
|
|
4189
|
+
def refresh() -> None:
|
|
4190
|
+
controller.stopping.set()
|
|
4191
|
+
|
|
4192
|
+
monkeypatch.setattr(controller.stopping, "wait", wait)
|
|
4193
|
+
monkeypatch.setattr(controller, "refresh", refresh)
|
|
4194
|
+
monkeypatch.setattr(controller, "_clear_stale_update_cache", lambda: None)
|
|
4195
|
+
monkeypatch.setattr("wintray.app.time.time", lambda: 1000.0)
|
|
4196
|
+
controller._poll_loop()
|
|
4197
|
+
|
|
4198
|
+
assert waits == [60]
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import socket
|
|
12
|
+
import threading
|
|
13
|
+
import urllib.request
|
|
14
|
+
from concurrent.futures import Future
|
|
15
|
+
from concurrent.futures import TimeoutError as FutureTimeoutError
|
|
12
16
|
|
|
13
17
|
# The host the Antigravity CLI refreshes its OAuth token against.
|
|
14
18
|
_GOOGLE_AUTH_HOST = "oauth2.googleapis.com"
|
|
@@ -21,9 +25,27 @@ def google_auth_reachable(timeout: float = _PROBE_TIMEOUT_SECONDS) -> bool:
|
|
|
21
25
|
Right after a sleep wake the network is often still down. The Antigravity CLI
|
|
22
26
|
treats a failed token refresh as "not logged in" and opens a browser sign-in
|
|
23
27
|
page, so callers skip launching it while this returns False.
|
|
28
|
+
|
|
29
|
+
A proxy-only network blocks this direct probe even though the CLI can connect
|
|
30
|
+
through the proxy, so a configured HTTPS proxy counts as reachable. A bare
|
|
31
|
+
``NO_PROXY`` does not route anything through a proxy and keeps the probe.
|
|
24
32
|
"""
|
|
33
|
+
proxies = urllib.request.getproxies()
|
|
34
|
+
if proxies.get("https") or proxies.get("all"):
|
|
35
|
+
return True
|
|
36
|
+
result: Future[bool] = Future()
|
|
37
|
+
|
|
38
|
+
def probe() -> None:
|
|
39
|
+
try:
|
|
40
|
+
with socket.create_connection((_GOOGLE_AUTH_HOST, 443), timeout=timeout):
|
|
41
|
+
result.set_result(True)
|
|
42
|
+
except OSError:
|
|
43
|
+
result.set_result(False)
|
|
44
|
+
except BaseException as exc:
|
|
45
|
+
result.set_exception(exc)
|
|
46
|
+
|
|
47
|
+
threading.Thread(target=probe, daemon=True).start()
|
|
25
48
|
try:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
except OSError:
|
|
49
|
+
return result.result(timeout=timeout)
|
|
50
|
+
except FutureTimeoutError:
|
|
29
51
|
return False
|
|
@@ -1437,12 +1437,18 @@ class _WindowsTrayController:
|
|
|
1437
1437
|
def _poll_loop(self) -> None:
|
|
1438
1438
|
current_thread = threading.current_thread()
|
|
1439
1439
|
self._poll_thread = current_thread
|
|
1440
|
+
waited = 0.0
|
|
1441
|
+
fast = self._poll_fast()
|
|
1440
1442
|
try:
|
|
1441
|
-
while
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1443
|
+
# Wake every interval even while hidden, so a countdown or panel that
|
|
1444
|
+
# appears mid-wait refreshes promptly instead of after the slow interval.
|
|
1445
|
+
# A countdown that just ended still gets one refresh to clear its label.
|
|
1446
|
+
while not self.stopping.wait(self.interval):
|
|
1447
|
+
waited += self.interval
|
|
1448
|
+
was_fast, fast = fast, self._poll_fast()
|
|
1449
|
+
if not (fast or was_fast) and waited < SLOW_POLL_INTERVAL_S:
|
|
1450
|
+
continue
|
|
1451
|
+
waited = 0.0
|
|
1446
1452
|
self.refresh()
|
|
1447
1453
|
self._clear_stale_update_cache()
|
|
1448
1454
|
if self._auto_check_schedule.try_begin(time.time()):
|
|
@@ -1451,6 +1457,9 @@ class _WindowsTrayController:
|
|
|
1451
1457
|
if self._poll_thread is current_thread:
|
|
1452
1458
|
self._poll_thread = None
|
|
1453
1459
|
|
|
1460
|
+
def _poll_fast(self) -> bool:
|
|
1461
|
+
return self.visible or (self.quota_label_enabled and self._tray_countdown() is not None)
|
|
1462
|
+
|
|
1454
1463
|
def _ensure_windows_watcher(self) -> None:
|
|
1455
1464
|
if self.mock or self.stopping.is_set():
|
|
1456
1465
|
return
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
-
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import socket
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
from usage_common import network
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class _Connection:
|
|
14
|
-
def __enter__(self) -> _Connection:
|
|
15
|
-
return self
|
|
16
|
-
|
|
17
|
-
def __exit__(self, *exc: object) -> None:
|
|
18
|
-
return None
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_reachable_when_connection_opens(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
22
|
-
seen: list[tuple[tuple[str, int], float]] = []
|
|
23
|
-
|
|
24
|
-
def connect(address: tuple[str, int], timeout: float) -> _Connection:
|
|
25
|
-
seen.append((address, timeout))
|
|
26
|
-
return _Connection()
|
|
27
|
-
|
|
28
|
-
monkeypatch.setattr(socket, "create_connection", connect)
|
|
29
|
-
|
|
30
|
-
assert network.google_auth_reachable(timeout=1.5) is True
|
|
31
|
-
assert seen == [(("oauth2.googleapis.com", 443), 1.5)]
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@pytest.mark.parametrize("error", [socket.gaierror("no dns"), TimeoutError(), OSError("down")])
|
|
35
|
-
def test_unreachable_when_connection_fails(monkeypatch: pytest.MonkeyPatch, error: OSError) -> None:
|
|
36
|
-
def connect(address: tuple[str, int], timeout: float) -> _Connection:
|
|
37
|
-
raise error
|
|
38
|
-
|
|
39
|
-
monkeypatch.setattr(socket, "create_connection", connect)
|
|
40
|
-
|
|
41
|
-
assert network.google_auth_reachable() is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|