File size: 7,319 Bytes
b46b0ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8d79e1
b46b0ef
 
 
 
 
 
 
 
 
eef20d7
 
bd2c14a
 
eef20d7
bd2c14a
eef20d7
 
bd2c14a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eef20d7
7880373
bd2c14a
 
 
 
 
7880373
bd2c14a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8d79e1
474b520
bd2c14a
f8d79e1
 
7880373
bd2c14a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7880373
bd2c14a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8d79e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""Unit tests for dashboard/components.py HTML helpers."""
from __future__ import annotations
import pytest
from unittest.mock import patch, MagicMock


def test_ai_badge_default_label():
    from dashboard.components import ai_badge
    html = ai_badge()
    assert "✦ AI Synthesis" in html
    assert "#7c3aed" in html
    assert "#ede9fe" in html


def test_ai_badge_custom_label():
    from dashboard.components import ai_badge
    html = ai_badge("AI")
    assert "✦ AI" in html
    assert "#7c3aed" in html


def test_interpretation_card_contains_badge_and_content():
    from dashboard.components import interpretation_card
    html = interpretation_card("What matters most", "<p>Some insight</p>")
    assert "✦ AI Synthesis" in html
    assert "#8b5cf6" in html   # purple border (PURPLE token value)
    # card background is now white (#ffffff), not lavender AI_BG
    assert "What matters most" in html
    assert "<p>Some insight</p>" in html


def test_interpretation_card_custom_badge_label():
    from dashboard.components import interpretation_card
    html = interpretation_card("Language shift", "<p>tone changed</p>", badge_label="AI")
    assert "✦ AI" in html
    assert "Language shift" in html


def _render_signal(sig) -> str:
    """Render a Signal through signal_card and return the produced HTML."""
    import streamlit as st
    from dashboard.components import signal_card

    with patch.object(st, "markdown") as mock_md:
        signal_card(sig)
        return mock_md.call_args[0][0]


def test_signal_card_tension_variant():
    """Tension variant: two-sided body, exactly one ✦ AI badge in the header."""
    from dashboard.signal_feed import Signal

    html = _render_signal(Signal(
        kind="tension",
        headline="Revenue beat hides quality decline",
        stance="mixed",
        significance="HIGH",
        extra={
            "bullish_reading": "Strong top-line momentum",
            "bearish_reading": "One-time item inflated result",
            "bullish_evidence": {"evidence_snippet": "q1", "reliability": "HIGH", "source": "10-Q"},
            "bearish_evidence": {"evidence_snippet": "q2", "reliability": "HIGH", "source": "10-Q"},
        },
    ))

    assert "✦ AI · experimental" in html
    assert html.count("✦ AI") == 1  # badge is in the outer header, not per reading panel
    assert "Strong top-line momentum" in html
    assert "One-time item inflated result" in html
    assert "Surface reading" in html
    assert "Deeper reading" in html
    assert "Material" in html             # priority label, not raw HIGH/MED/LOW
    # mixed stance β†’ amber accent, never bull green or bear red on the frame
    assert "#f59e0b" in html


def test_signal_card_quality_variant():
    """Quality variant: assessment chip + rationale + AI badge (AI-assessed kind)."""
    from dashboard.signal_feed import Signal

    html = _render_signal(Signal(
        kind="quality",
        body="Management raised full-year guidance for the third consecutive quarter.",
        stance="bull",
        category="guidance_dynamics",
        extra={"assessment": "positive"},
        evidence_snippet="raised guidance",
        source="10-Q", reliability="HIGH",
    ))

    assert "Management raised full-year guidance" in html
    assert "Guidance" in html          # dimension label for guidance_dynamics
    assert "β–²" in html
    assert "Positive" in html
    assert "✦ AI · experimental" in html  # quality assessments are experimental AI
    assert "#10b981" in html           # bull stance β†’ green accent


def test_signal_card_delta_variant_redline():
    """Delta variant: computed-metric box + before/after redline, no AI badge."""
    from dashboard.signal_feed import Signal

    html = _render_signal(Signal(
        kind="delta",
        headline="export controls",
        body="2β†’8 occurrences (+300%)",
        stance="bear",
        significance="HIGH",
        category="term_frequency",
        before_text="limited exposure to export restrictions",
        after_text="new export restrictions may materially affect revenue",
        source="10-Q",
        period_range="4Q2024 β†’ 1Q2025",
    ))

    assert "computed" in html
    assert "2β†’8 occurrences" in html
    assert "before" in html and "after" in html
    assert "4Q2024 β†’ 1Q2025" in html
    assert "✦ AI" not in html          # deltas are deterministic, not AI
    assert "Heuristic Β· validate" in html


def test_signal_card_risk_new_badge():
    """Risk variant: category chip + NEW badge when is_new."""
    from dashboard.signal_feed import Signal

    html = _render_signal(Signal(
        kind="risk",
        body="China exposure may affect supply chains.",
        stance="bear",
        significance="HIGH",
        category="Geopolitical",
        is_new=True,
        source="10-K", reliability="HIGH", impact="HIGH",
    ))

    assert "Geopolitical" in html
    assert "NEW" in html
    assert "#ef4444" in html           # bear stance β†’ red accent


# ── New helper tests ──────────────────────────────────────────────────────────

def test_eyebrow_label():
    from dashboard.components import eyebrow_label

    html = eyebrow_label("what changed")
    assert "text-transform:uppercase" in html
    assert "what changed" in html
    assert "letter-spacing" in html

    html_colored = eyebrow_label("section", color="#ef4444")
    assert "#ef4444" in html_colored


def test_importance_marker_levels():
    from dashboard.components import importance_marker

    high = importance_marker("HIGH")
    assert "Critical" in high
    assert "#0f172a" in high
    assert "●" in high
    # monochrome only β€” no green or red signal colors
    assert "#10b981" not in high
    assert "#ef4444" not in high

    medium = importance_marker("MEDIUM")
    assert "Important" in medium
    assert "#64748b" in medium
    assert "●" in medium

    low = importance_marker("LOW")
    assert "Context" in low
    assert "#94a3b8" in low
    assert "●" in low

    # Unknown / empty returns empty string
    assert importance_marker("") == ""


def test_meta_row_formatting():
    from dashboard.components import meta_row

    result = meta_row("HIGH", "10-Q", "HIGH")
    assert "High confidence" in result
    assert "10-Q" in result
    assert "Critical" in result  # from importance_marker("HIGH")

    # All empty β†’ empty string
    assert meta_row("", "", "") == ""

    result2 = meta_row("MEDIUM", "transcript", "LOW")
    assert "Med. confidence" in result2
    assert "transcript" in result2
    assert "Context" in result2  # from importance_marker("LOW")


def test_sort_by_impact_order():
    from dashboard.components import sort_by_impact

    items = [
        {"impact": "HIGH",   "name": "h"},
        {"impact": "LOW",    "name": "l"},
        {"impact": "MEDIUM", "name": "m"},
        {"name": "none"},               # missing "impact" key
    ]
    original = list(items)  # copy to check mutation

    sorted_items = sort_by_impact(items)

    assert [i.get("impact", "") for i in sorted_items] == ["HIGH", "MEDIUM", "LOW", ""]

    # Original list must not be mutated
    assert items == original