CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
143 lines
4.7 KiB
Python
143 lines
4.7 KiB
Python
"""EPIC R08-T13: TokenUsageCardWidget / UsageChartWidget / HabitsWidget /
|
|
DashboardTab shell, real Qt offscreen.
|
|
|
|
``DashboardQueryService``'s own logic is unit tested (R08-T13, no Qt) in
|
|
``tests/unit/test_dashboard_query_service.py``; this file proves the three
|
|
widgets and the shell are actually wired to it and to each other (the period
|
|
selector living on ``UsageChartWidget`` driving all three).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from datetime import date
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
|
|
from cowork_local.config import AppConfig # noqa: E402
|
|
from cowork_local.core import usage_tracker as ut # noqa: E402
|
|
from cowork_local.state import AppContext # noqa: E402
|
|
|
|
pytest.importorskip("PySide6", reason="Qt is required for the integration suite")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def qt_app():
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
return QApplication.instance() or QApplication([])
|
|
|
|
|
|
@pytest.fixture
|
|
def usage_dir(tmp_path, monkeypatch):
|
|
d = tmp_path / "usage"
|
|
monkeypatch.setattr(ut, "USAGE_DIR", d)
|
|
return d
|
|
|
|
|
|
@pytest.fixture
|
|
def ctx(qt_app, tmp_path):
|
|
return AppContext(AppConfig.load(tmp_path / "config.json"))
|
|
|
|
|
|
def _write_event(usage_dir, day: date, **overrides):
|
|
usage_dir.mkdir(parents=True, exist_ok=True)
|
|
event = {
|
|
"ts": f"{day.isoformat()}T10:00:00", "source": "cowork", "label": "Test chat",
|
|
"provider": "anthropic", "model": "claude-sonnet-4-6",
|
|
"in": 100, "out": 50, "cache": 0, "estimated": False,
|
|
"account": "", "machine": "",
|
|
}
|
|
event.update(overrides)
|
|
path = usage_dir / f"{day.isoformat()}.jsonl"
|
|
with path.open("a", encoding="utf-8") as f:
|
|
f.write(json.dumps(event) + "\n")
|
|
|
|
|
|
def test_dashboard_tab_builds_and_populates_cards(usage_dir, ctx):
|
|
from cowork_local.presentation.dashboard.dashboard_tab import DashboardTab
|
|
|
|
_write_event(usage_dir, date.today())
|
|
tab = DashboardTab(ctx) # refresh() runs once at construction
|
|
|
|
assert "100" in tab.token_cards.card_in.value_lbl.text() \
|
|
or tab.token_cards.card_in.value_lbl.text() # non-crashing, has SOME text
|
|
assert tab.token_cards.card_total.value_lbl.text() != ""
|
|
|
|
|
|
def test_period_navigation_on_chart_refreshes_the_whole_shell(usage_dir, ctx):
|
|
from cowork_local.presentation.dashboard.dashboard_tab import DashboardTab
|
|
|
|
_write_event(usage_dir, date.today())
|
|
tab = DashboardTab(ctx)
|
|
calls = []
|
|
tab.refresh = lambda *a, orig=tab.refresh: (calls.append(1), orig(*a))[-1]
|
|
|
|
tab.chart._chart_prev()
|
|
|
|
assert calls == [1]
|
|
|
|
|
|
def test_granularity_change_resets_offset_to_current(usage_dir, ctx):
|
|
from cowork_local.presentation.dashboard.dashboard_tab import DashboardTab
|
|
from cowork_local.presentation.dashboard.usage_chart_widget import UsageChartWidget
|
|
|
|
tab = DashboardTab(ctx)
|
|
tab.chart._chart_offset = -3
|
|
|
|
idx = tab.chart.gran_combo.findData("month")
|
|
tab.chart.gran_combo.setCurrentIndex(idx)
|
|
|
|
assert tab.chart.chart_offset == 0
|
|
|
|
|
|
def test_currency_change_persists_and_triggers_refresh(usage_dir, ctx):
|
|
from cowork_local.presentation.dashboard.dashboard_tab import DashboardTab
|
|
|
|
tab = DashboardTab(ctx)
|
|
idx = tab.chart.currency_combo.findData("EUR") \
|
|
if tab.chart.currency_combo.findData("EUR") >= 0 else 0
|
|
seen = []
|
|
tab.chart.currency_changed.connect(lambda: seen.append(1))
|
|
|
|
tab.chart.currency_combo.setCurrentIndex(idx)
|
|
|
|
if tab.chart.currency_combo.currentData() != "USD":
|
|
assert seen == [1]
|
|
assert ctx.config.data["usage"]["currency"] == tab.chart.currency_combo.currentData()
|
|
|
|
|
|
def test_habits_widget_ai_analyze_noop_without_data(usage_dir, ctx):
|
|
"""No events in range -> emits a status message instead of starting a
|
|
background worker (matches the original _ai_analyze guard)."""
|
|
from cowork_local.presentation.dashboard.habits_widget import HabitsWidget
|
|
from cowork_local.application.monitoring import DashboardQueryService
|
|
|
|
query = DashboardQueryService(ctx)
|
|
widget = HabitsWidget(ctx, query)
|
|
widget.refresh(date(2020, 1, 1), date(2020, 1, 1))
|
|
messages = []
|
|
widget.status_message.connect(messages.append)
|
|
|
|
widget._ai_analyze()
|
|
|
|
assert messages # "no data" status, no worker started
|
|
assert widget._ai_worker is None
|
|
|
|
|
|
def test_budget_apply_updates_budget_card(usage_dir, ctx):
|
|
from cowork_local.application.monitoring import DashboardQueryService
|
|
from cowork_local.presentation.dashboard.token_usage_card_widget import TokenUsageCardWidget
|
|
|
|
query = DashboardQueryService(ctx)
|
|
widget = TokenUsageCardWidget(ctx, query)
|
|
widget.budget_card.budget_spin.setValue(50.0)
|
|
|
|
widget._apply_budget()
|
|
|
|
status = query.budget_status()
|
|
assert status is not None
|
|
assert status["amount_usd"] == pytest.approx(50.0)
|