316 lines
13 KiB
Python
316 lines
13 KiB
Python
"""Static smoke tests for Usage view cost provenance display."""
|
|
|
|
from pathlib import Path
|
|
|
|
USAGE_JS = Path("src/opensquilla/gateway/static/js/views/usage.js")
|
|
USAGE_CSS = Path("src/opensquilla/gateway/static/css/views/usage.css")
|
|
COMPONENTS_CSS = Path("src/opensquilla/gateway/static/css/components.css")
|
|
|
|
|
|
def test_usage_view_renders_cost_source_badges_and_exports_fields() -> None:
|
|
source = USAGE_JS.read_text(encoding="utf-8")
|
|
|
|
assert "_renderCostSourceBadge(row)" in source
|
|
assert "{ key: 'cost_source', label: 'Source' }" in source
|
|
assert "billed_cost_usd" in source
|
|
assert "estimated_cost_usd" in source
|
|
assert "missing_cost_entries" in source
|
|
assert "cost_ephemeral" in source
|
|
|
|
|
|
def test_usage_sessions_default_to_modified_time_sort() -> None:
|
|
source = USAGE_JS.read_text(encoding="utf-8")
|
|
|
|
assert "let _sortCol = 'updated_at';" in source
|
|
assert "{ key: 'updated_at', label: 'Modified' }" in source
|
|
assert "case 'updated_at':" in source
|
|
assert "return _sessionTimestamp(row) || 0;" in source
|
|
assert "'updated_at', 'input_tokens'" in source
|
|
assert "const modified = timestamp != null ? UI.relTime(timestamp) : '—';" in source
|
|
|
|
|
|
def test_usage_collapsed_model_display_uses_model_breakdown() -> None:
|
|
source = USAGE_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _renderModelCell(row)")
|
|
end = source.index(" function _buildExpandedContent(row)", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _modelDisplayLabel(row)" in source
|
|
assert "bd.length > 1 ? `auto · ${bd.length} models`" in source
|
|
assert "bd[0].model || row.model" in source
|
|
assert "const label = _modelDisplayLabel(row);" in body
|
|
assert "const label = bd.length > 1 ? `auto · ${bd.length} models` : _esc(model);" not in body
|
|
|
|
|
|
def test_usage_model_card_identifiers_wrap_inside_mobile_cards() -> None:
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
provider_start = source.index(".usage-model-card__provider {")
|
|
provider_rule = source[provider_start : source.index("}", provider_start)]
|
|
name_start = source.index(".usage-model-card__name {")
|
|
name_rule = source[name_start : source.index("}", name_start)]
|
|
|
|
for rule in (provider_rule, name_rule):
|
|
assert "max-width: 100%" in rule
|
|
assert "min-width: 0" in rule
|
|
assert "overflow-wrap: anywhere" in rule
|
|
|
|
assert "white-space: normal" in name_rule
|
|
assert "text-overflow: clip" in name_rule
|
|
|
|
|
|
def test_usage_view_has_cost_source_styles() -> None:
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
|
|
assert ".usage-source--provider_billed" in source
|
|
# New pro-rated style for the billed-but-split breakdown items.
|
|
assert ".usage-source--provider_billed_prorated" in source
|
|
assert ".usage-source--opensquilla_estimate" in source
|
|
assert ".usage-source--mixed" in source
|
|
assert ".usage-source--unavailable" in source
|
|
assert ".usage-source--ephemeral" in source
|
|
|
|
|
|
def test_usage_segmented_controls_keep_touch_friendly_hit_areas() -> None:
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
currency_start = source.index(".usage-currency__btn {")
|
|
seg_start = source.index(".usage-seg, .usage-range__btn {")
|
|
currency_rule = source[
|
|
currency_start : source.index("}", currency_start)
|
|
]
|
|
seg_rule = source[seg_start : source.index("}", seg_start)]
|
|
|
|
assert "min-height: 40px" in currency_rule
|
|
assert "min-height: 40px" in seg_rule
|
|
|
|
|
|
def test_usage_active_segmented_controls_use_theme_contrast_token() -> None:
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
currency_start = source.index(".usage-currency__btn.is-active {")
|
|
seg_start = source.index(".usage-seg.is-active {")
|
|
currency_rule = source[currency_start : source.index("}", currency_start)]
|
|
seg_rule = source[seg_start : source.index("}", seg_start)]
|
|
|
|
assert "color: var(--accent-foreground)" in currency_rule
|
|
assert "color: var(--accent-foreground)" in seg_rule
|
|
|
|
|
|
def test_usage_header_actions_stay_single_row_scrollable_on_mobile() -> None:
|
|
components = COMPONENTS_CSS.read_text(encoding="utf-8")
|
|
js = USAGE_JS.read_text(encoding="utf-8")
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
|
|
mobile_start = source.rindex("@media (max-width: 720px)")
|
|
mobile_block = source[mobile_start:]
|
|
|
|
assert ".mobile-action-strip {" in components
|
|
assert ".mobile-action-strip.mobile-action-strip { flex-wrap: nowrap; }" in components
|
|
assert ".mobile-action-strip__button {" in components
|
|
assert ".mobile-action-strip__label {" in components
|
|
assert 'usage-stage__actions mobile-action-strip' in js
|
|
assert 'usage-currency mobile-action-strip__item' in js
|
|
assert 'mobile-action-strip__button' in js
|
|
assert 'mobile-action-strip__label' in js
|
|
assert "--mobile-action-button-size: 44px" in source
|
|
assert ".usage-stage__actions .btn span" not in source
|
|
|
|
button_start = mobile_block.rindex(".usage-stage__actions .btn {")
|
|
button_rule = mobile_block[button_start : mobile_block.index("}", button_start)]
|
|
assert "background: var(--bg-surface)" in button_rule
|
|
assert "border-color: var(--border)" in button_rule
|
|
|
|
|
|
def test_usage_chart_labels_wrap_on_mobile() -> None:
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
|
|
mobile_start = source.rindex("@media (max-width: 720px)")
|
|
mobile_block = source[mobile_start:]
|
|
row_start = mobile_block.index(".usage-bar-row {")
|
|
row_rule = mobile_block[row_start : mobile_block.index("}", row_start)]
|
|
label_start = mobile_block.index(".usage-bar-row__label {")
|
|
label_rule = mobile_block[label_start : mobile_block.index("}", label_start)]
|
|
|
|
assert "grid-template-columns: minmax(0, 1fr) auto" in row_rule
|
|
assert "align-items: start" in row_rule
|
|
assert "row-gap: 6px" in row_rule
|
|
assert "grid-column: 1 / -1" in label_rule
|
|
assert "max-width: 100%" in label_rule
|
|
assert "white-space: normal" in label_rule
|
|
assert "overflow-wrap: anywhere" in label_rule
|
|
assert "text-overflow: clip" in label_rule
|
|
|
|
|
|
def test_usage_empty_table_state_stays_visible_on_mobile() -> None:
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
|
|
empty_start = source.index(".usage-empty-row .state {")
|
|
empty_rule = source[empty_start : source.index("}", empty_start)]
|
|
mobile_start = source.index("@media (max-width: 720px)")
|
|
mobile_block = source[mobile_start:]
|
|
mobile_empty_start = mobile_block.index(".usage-empty-row .state {")
|
|
mobile_empty_rule = mobile_block[
|
|
mobile_empty_start : mobile_block.index("}", mobile_empty_start)
|
|
]
|
|
|
|
assert "width: min(100%, 520px)" in empty_rule
|
|
assert "margin-inline: auto" in empty_rule
|
|
assert "width: min(100%, calc(100vw - 64px))" in mobile_empty_rule
|
|
assert "margin-inline: 0" in mobile_empty_rule
|
|
|
|
|
|
def test_usage_table_scroll_edge_has_visual_affordance() -> None:
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
|
|
wrap_start = source.index(".usage-table-wrap {")
|
|
wrap_rule = source[wrap_start : source.index("}", wrap_start)]
|
|
tablet_start = source.index("@media (max-width: 900px)")
|
|
next_mobile_start = source.index("@media (max-width: 720px)", tablet_start)
|
|
tablet_block = source[tablet_start:next_mobile_start]
|
|
fade_start = tablet_block.index(".usage-table-wrap::after {")
|
|
fade_rule = tablet_block[fade_start : tablet_block.index("}", fade_start)]
|
|
|
|
assert "position: relative" in wrap_rule
|
|
assert "overflow-x: auto" in wrap_rule
|
|
assert 'content: ""' in fade_rule
|
|
assert "position: absolute" in fade_rule
|
|
assert "right: 0" in fade_rule
|
|
assert "pointer-events: none" in fade_rule
|
|
assert "linear-gradient" in fade_rule
|
|
assert "box-shadow" in fade_rule
|
|
|
|
|
|
def test_usage_session_table_becomes_labeled_cards_on_mobile() -> None:
|
|
js = USAGE_JS.read_text(encoding="utf-8")
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
|
|
mobile_start = source.rindex("@media (max-width: 720px)")
|
|
mobile_block = source[mobile_start:]
|
|
|
|
for label in [
|
|
"Session",
|
|
"Modified",
|
|
"Input",
|
|
"Output",
|
|
"Cache R",
|
|
"Cache W",
|
|
"Cost",
|
|
"Source",
|
|
"Model",
|
|
]:
|
|
assert f'data-label="{label}"' in js
|
|
|
|
assert ".usage-table-wrap { overflow-x: hidden; }" in mobile_block
|
|
assert ".usage-table-wrap::after { content: none; }" in mobile_block
|
|
assert ".usage-table thead {" in mobile_block
|
|
assert "clip: rect(0 0 0 0)" in mobile_block
|
|
assert "clip-path: inset(50%)" in mobile_block
|
|
assert ".usage-table thead tr," in mobile_block
|
|
assert ".usage-table tbody {" in mobile_block
|
|
assert "display: grid" in mobile_block
|
|
assert ".usage-table tbody td {" in mobile_block
|
|
assert "grid-template-columns: minmax(72px, 0.34fr) minmax(0, 1fr)" in mobile_block
|
|
assert "content: attr(data-label)" in mobile_block
|
|
assert "usage-expand-cell" in js
|
|
assert ".usage-expand-cell::before" in mobile_block
|
|
|
|
|
|
def test_usage_view_recognises_prorated_source() -> None:
|
|
"""The cost-source label/tooltip switch must handle provider_billed_prorated.
|
|
|
|
UI choice: the badge text stays "Actual" (the total IS the real billed
|
|
amount; only the per-model split is estimated). The visual differentiation
|
|
is the dashed-border CSS variant and the tooltip explaining the nuance.
|
|
"""
|
|
source = USAGE_JS.read_text(encoding="utf-8")
|
|
assert "case 'provider_billed_prorated':" in source
|
|
# Tooltip must call out the split-is-estimated nuance without resorting
|
|
# to billing-period terms like "pro-rated" which carry misleading
|
|
# connotations of partial-time refunds.
|
|
assert "Total is real billed" in source
|
|
assert "per-model split is estimated" in source
|
|
assert "'provider_billed_prorated'" in source # in _costSourceClass known list
|
|
|
|
|
|
def test_usage_expand_row_renders_cost_source_badge() -> None:
|
|
"""Per-model expand rows must surface a Source badge.
|
|
|
|
Without a per-row badge, the pro-rated source is invisible to the user.
|
|
Without this assertion a regression could re-hide the per-model source by
|
|
accidentally removing the cell.
|
|
"""
|
|
source = USAGE_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _buildExpandedContent(row)")
|
|
end = source.index("\n function ", start + 1)
|
|
body = source[start:end]
|
|
|
|
assert "usage-expand__source" in body
|
|
assert "_renderCostSourceBadge(m)" in body
|
|
# The grouped disclosure shown when any item is pro-rated.
|
|
assert "usage-expand__notice" in body
|
|
# Disclosure copy: must mention that the split is estimated.
|
|
assert "split is estimated" in body
|
|
|
|
|
|
def test_usage_view_has_expand_source_styles() -> None:
|
|
source = USAGE_CSS.read_text(encoding="utf-8")
|
|
# Desktop grid must include the Source column.
|
|
assert ".usage-expand__source" in source
|
|
assert "usage-expand__notice" in source
|
|
|
|
|
|
def test_usage_view_range_selector_is_page_wide() -> None:
|
|
source = USAGE_JS.read_text(encoding="utf-8")
|
|
|
|
assert 'data-range="all"' in source
|
|
assert "let _range" in source
|
|
assert "_visibleSessions()" in source
|
|
assert "Number(btn.dataset.range)" not in source
|
|
# _renderMetrics dropped its unused `cost` parameter when usage.cost was
|
|
# removed from the polling loop; usage.cost RPC still exists for CLI / chat
|
|
# / HTTP consumers — the view just doesn't fetch it twice per poll.
|
|
assert "_renderMetrics(_lastStatus)" in source
|
|
assert "_lastCost" not in source
|
|
assert "_rpc.call('usage.cost')" not in source
|
|
assert "_renderTable()" in source
|
|
assert "_renderChart()" in source
|
|
assert "_renderModelBreakdown()" in source
|
|
|
|
|
|
def test_usage_view_visible_session_helper_drives_renderers_and_export() -> None:
|
|
source = USAGE_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _sessionTimestamp(row)" in source
|
|
assert "function _rangeCutoffMs" in source
|
|
assert "function _visibleSessions()" in source
|
|
assert "function _undatedHiddenCount()" in source
|
|
assert "function _usageTotals(rows)" in source
|
|
assert "undated legacy session" in source
|
|
|
|
for marker in [
|
|
"function _renderMetrics(status)",
|
|
"function _renderTable()",
|
|
"function _renderChart()",
|
|
"function _renderModelBreakdown()",
|
|
"function _exportCsv()",
|
|
]:
|
|
start = source.index(marker)
|
|
body = source[start : source.index("\n function ", start + 1)]
|
|
assert "_visibleSessions()" in body or "visibleRows" in body
|
|
|
|
|
|
def test_usage_view_model_expansion_uses_visible_sessions() -> None:
|
|
source = USAGE_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _bindModelToggles(wrap)")
|
|
end = source.index(" function _renderModelBreakdown()", start)
|
|
body = source[start:end]
|
|
|
|
assert "_visibleSessions().find" in body
|
|
|
|
|
|
def test_usage_expand_row_colspan_tracks_session_table_columns() -> None:
|
|
source = USAGE_JS.read_text(encoding="utf-8")
|
|
|
|
assert "const USAGE_SESSION_TABLE_COLUMNS" in source
|
|
assert "USAGE_SESSION_TABLE_COLUMNS.forEach" in source
|
|
assert "td.colSpan = USAGE_SESSION_TABLE_COLUMNS.length" in source
|
|
assert "td.colSpan = (typeof cols" not in source
|