Files
cowork-local/tests/integration/test_dashboard_tab.py
T
f9f6bc01fd
CI / test (push) Canceled after 0s
Feature/delta team/epic r04 (#7)
## 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>
2026-08-31 05:15:13 +00:00

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)