CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 29s
CI & Build / Python tests (push) Successful in 1m3s
CI & Build / Build & push image (push) Successful in 32s
task_kind was only visible inside the task editor's Kind select, so every list surface rendered work, issue and spike identically and a list of tasks hid the fact that three different things were in it. ONE component, not a fifth spelling. The badge layer had already drifted — StatusBadge.vue is the recorded canon (#2960) but WorkspaceTaskPanel, ProjectView and KnowledgeView each carry their own scoped `.status-badge`. KindBadge is modelled on PriorityBadge, its closest sibling, which already does the thing that matters here: the DEFAULT value renders nothing. `work` is most tasks, so badging it would put a chip on nearly every row and say nothing — the same reason RuleListPane marks only `conditional`. COLOUR BY TEMPERATURE, measured rather than eyeballed. Issue and spike are opposite in character — corrective vs exploratory — so they split warm (warning) against cool (info), which survives being small and stays distinguishable without reading the word. Neither uses the accent; kind is not one of the places it is allowed. The raw semantic colour FAILS the contrast floor on the dark palette: warning on its own 12% tint measures 2.97:1 against AA's 4.5. So the text is the hue mixed toward --fs-text-primary, which passes and, because that token inverts by mode, follows light/dark for free. Measured both ways — issue 5.23:1 dark / 6.68:1 light, spike 5.33:1 / 9.26:1. `plan` renders hue-free and italic: retired since 0066, so a legacy row should read as archival rather than as a fourth kind competing for attention. In KnowledgeView it is passed as null instead, because the type badge beside it already says "Plan" and two chips reading the same word would look like two facts. Weight is 500, not the 600 the two older badges use — the house style allows 400 and 500 only, and copying 600 would spread it. SERVER FIX, without which this was decorative: dashboard's `_task_row` omitted task_kind entirely. The badge would have rendered nothing there while working everywhere else, which reads as "this list has no issues" rather than as a missing field. The guard is on the payload, where the omission was. Surfaces: ProjectView's three status columns, WorkspaceTaskPanel's two task lists, DashboardView's milestone and no-milestone rows, KnowledgeView's result rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""build_dashboard composition + helpers (services/dashboard.py).
|
|
|
|
Query semantics (ranking/caps/owner-scope) are exercised by manual smoke —
|
|
the repo's unit tests mock the DB, so SQL isn't executed here. These cover the
|
|
pure helpers and the section-isolation contract.
|
|
"""
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
def test_task_row_maps_fields():
|
|
from scribe.services.dashboard import _task_row
|
|
n = MagicMock()
|
|
n.id = 5
|
|
n.title = "Wire reminders"
|
|
n.status = "in_progress"
|
|
n.priority = None
|
|
# Named, not left to MagicMock: an unset attribute is a truthy Mock, so
|
|
# the assertion would pass on a field the row never really carried
|
|
# (note 2109 — the reason fake_note exists).
|
|
n.task_kind = "spike"
|
|
assert _task_row(n) == {
|
|
"id": 5, "title": "Wire reminders", "status": "in_progress",
|
|
"priority": "none", "task_kind": "spike",
|
|
}
|
|
|
|
|
|
def test_task_row_carries_the_kind_so_a_row_can_show_it():
|
|
"""The field whose ABSENCE is invisible.
|
|
|
|
A dashboard row with no task_kind renders no kind badge, which looks
|
|
exactly like a list containing no issues and no spikes. The badge is
|
|
correct and the payload is short — so the guard belongs on the payload,
|
|
where the omission actually was.
|
|
"""
|
|
from scribe.services.dashboard import _task_row
|
|
n = MagicMock()
|
|
n.id = 1
|
|
n.title = "t"
|
|
n.status = "todo"
|
|
n.priority = "none"
|
|
n.task_kind = "issue"
|
|
assert _task_row(n)["task_kind"] == "issue"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_safe_returns_value_then_empty_on_error():
|
|
from scribe.services.dashboard import _safe
|
|
|
|
async def ok():
|
|
return [1, 2, 3]
|
|
|
|
async def boom():
|
|
raise RuntimeError("section blew up")
|
|
|
|
assert await _safe(ok(), []) == [1, 2, 3]
|
|
# a failing section returns the supplied empty default, not an exception
|
|
assert await _safe(boom(), []) == []
|
|
assert await _safe(boom(), {}) == {}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_build_dashboard_composes_sections():
|
|
import scribe.services.dashboard as dash
|
|
with patch.object(dash, "_active_projects", AsyncMock(return_value=["P"])), \
|
|
patch.object(dash, "_recently_completed", AsyncMock(return_value=["done"])), \
|
|
patch.object(dash, "_open_issues", AsyncMock(return_value=["iss"])), \
|
|
patch.object(dash, "_week_stats", AsyncMock(return_value={"open_total": 4})):
|
|
out = await dash.build_dashboard(user_id=1)
|
|
assert out == {
|
|
"active_projects": ["P"],
|
|
"recently_completed": ["done"],
|
|
"open_issues": ["iss"],
|
|
"week_stats": {"open_total": 4},
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_build_dashboard_isolates_failing_section():
|
|
import scribe.services.dashboard as dash
|
|
with patch.object(dash, "_active_projects", AsyncMock(side_effect=RuntimeError("db down"))), \
|
|
patch.object(dash, "_recently_completed", AsyncMock(return_value=["done"])), \
|
|
patch.object(dash, "_open_issues", AsyncMock(return_value=[])), \
|
|
patch.object(dash, "_week_stats", AsyncMock(return_value={})):
|
|
out = await dash.build_dashboard(user_id=1)
|
|
# failing section degrades to its empty default; others still populate
|
|
assert out["active_projects"] == []
|
|
assert out["recently_completed"] == ["done"]
|