Skip to content

Commit 7c28277

Browse files
authored
Merge pull request #41 from teams-notifier/feat/open-threads-to-card-icon-color
feat: card icon color reflects open thread status
2 parents 40efb6d + 3f542e6 commit 7c28277

2 files changed

Lines changed: 80 additions & 2 deletions

File tree

cards/render.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ def render(
101101

102102
discussion_stats = mri.merge_request_extra_state.discussion_stats
103103

104+
if (
105+
discussion_stats
106+
and discussion_stats.threads_unresolved > 0
107+
and mri.merge_request_payload.object_attributes.action not in ("close", "merge")
108+
):
109+
icon_color = Teams_Color.WARNING
110+
104111
precalc = {
105112
"path_with_namespace": mri.merge_request_payload.project.path_with_namespace,
106113
"iid": mri.merge_request_payload.object_attributes.iid,

tests/test_card_render.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#!/usr/bin/env python3
22
"""Tests for cards/render.py - YAML template rendering."""
33

4+
from typing import Any
5+
46
from cards.render import render
57
from cards.render import yaml_escape_sq
8+
from db import DiscussionStats
69
from db import GitlabUser
710
from db import MergeRequestExtraState
811
from db import MergeRequestInfos
@@ -21,6 +24,9 @@ def make_mri(
2124
assignees: list[GLUser] | None = None,
2225
reviewers: list[GLUser] | None = None,
2326
approvers: list[str] | None = None,
27+
action: str = "open",
28+
draft: bool = False,
29+
discussion_stats: DiscussionStats | None = None,
2430
) -> MergeRequestInfos:
2531
"""Create a MergeRequestInfos for testing."""
2632
payload = MergeRequestPayload(
@@ -46,10 +52,10 @@ def make_mri(
4652
iid=1,
4753
title=title,
4854
created_at="2025-01-01 00:00:00 UTC",
49-
draft=False,
55+
draft=draft,
5056
state="opened",
5157
url="https://gitlab.example.com/test/project/-/merge_requests/1",
52-
action="open",
58+
action=action,
5359
updated_at="2025-01-01 00:00:00 UTC",
5460
detailed_merge_status="mergeable",
5561
head_pipeline_id=None,
@@ -70,6 +76,7 @@ def make_mri(
7076
approvers={},
7177
pipeline_statuses={},
7278
emojis={},
79+
discussion_stats=discussion_stats,
7380
)
7481

7582
if approvers:
@@ -215,3 +222,67 @@ def test_fingerprint_is_sha256(self):
215222
fp = compute_mri_fingerprint(mri)
216223
assert len(fp) == 64
217224
assert all(c in "0123456789abcdef" for c in fp)
225+
226+
227+
def find_icon_color(card: dict[str, Any]) -> str | None:
228+
"""Find the icon color in the adaptive card body."""
229+
for item in card.get("body", []):
230+
if item.get("type") == "ColumnSet":
231+
for col in item.get("columns", []):
232+
for inner in col.get("items", []):
233+
if inner.get("type") == "Icon":
234+
color = inner.get("color")
235+
return str(color) if color is not None else None
236+
return None
237+
238+
239+
class TestIconColor:
240+
"""Tests for icon color based on MR state and unresolved threads."""
241+
242+
def test_default_icon_color_no_discussion_stats(self):
243+
"""Icon should be accent when no discussion stats."""
244+
mri = make_mri()
245+
result = render(mri)
246+
assert find_icon_color(result) == "accent"
247+
248+
def test_default_icon_color_no_unresolved_threads(self):
249+
"""Icon should be accent when all threads resolved."""
250+
stats = DiscussionStats(threads_total=3, threads_resolved=3, threads_unresolved=0)
251+
mri = make_mri(discussion_stats=stats)
252+
result = render(mri)
253+
assert find_icon_color(result) == "accent"
254+
255+
def test_warning_icon_color_with_unresolved_threads(self):
256+
"""Icon should be warning (orange) when there are unresolved threads."""
257+
stats = DiscussionStats(threads_total=3, threads_resolved=1, threads_unresolved=2)
258+
mri = make_mri(discussion_stats=stats)
259+
result = render(mri)
260+
assert find_icon_color(result) == "warning"
261+
262+
def test_closed_mr_keeps_attention_regardless_of_threads(self):
263+
"""Closed MR should keep attention color, not be overridden by thread status."""
264+
stats = DiscussionStats(threads_total=3, threads_resolved=1, threads_unresolved=2)
265+
mri = make_mri(action="close", discussion_stats=stats)
266+
result = render(mri)
267+
assert find_icon_color(result) == "attention"
268+
269+
def test_merged_mr_keeps_good_regardless_of_threads(self):
270+
"""Merged MR should keep good color, not be overridden by thread status."""
271+
stats = DiscussionStats(threads_total=3, threads_resolved=1, threads_unresolved=2)
272+
mri = make_mri(action="merge", discussion_stats=stats)
273+
result = render(mri)
274+
assert find_icon_color(result) == "good"
275+
276+
def test_draft_mr_with_unresolved_threads_shows_warning(self):
277+
"""Draft MR with unresolved threads should show warning color."""
278+
stats = DiscussionStats(threads_total=2, threads_resolved=0, threads_unresolved=2)
279+
mri = make_mri(draft=True, discussion_stats=stats)
280+
result = render(mri)
281+
assert find_icon_color(result) == "warning"
282+
283+
def test_draft_mr_without_unresolved_threads_shows_default(self):
284+
"""Draft MR without unresolved threads should show default color."""
285+
stats = DiscussionStats(threads_total=2, threads_resolved=2, threads_unresolved=0)
286+
mri = make_mri(draft=True, discussion_stats=stats)
287+
result = render(mri)
288+
assert find_icon_color(result) == "default"

0 commit comments

Comments
 (0)