File size: 8,069 Bytes
4b8ea27
 
a1ca041
f5d46fe
734ea40
a1ca041
 
211342c
f5d46fe
211342c
f5d46fe
a1ca041
 
 
 
 
4b8ea27
 
 
f5d46fe
4b8ea27
 
734ea40
 
 
 
f5d46fe
 
 
 
4b8ea27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91e2e23
 
 
 
 
a1ca041
 
 
211342c
 
 
 
 
 
 
 
 
 
 
f5d46fe
 
 
7797512
 
 
f5d46fe
 
7797512
 
 
 
 
 
f5d46fe
 
 
7797512
f5d46fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187b06a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5d46fe
 
734ea40
 
 
 
 
f5d46fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734ea40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5d46fe
187b06a
 
a1ca041
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5d46fe
734ea40
 
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
from __future__ import annotations

from app import (
    CSS,
    handle_stage_nav,
    handle_generate,
    handle_grade,
    pending_activity_message,
    readable_current_artifact_markdown,
    render_activity_message,
    render_stage_score_gate_panel,
    render_stage_score_strip,
    stage_score_rows,
    workflow_progress_summary,
    workspace_visibility_updates,
)
from course_slide_factory.constants import STAGE_IDS
from course_slide_factory.models import IssueSeverity, IssueType
from course_slide_factory.quality import approve_current_artifact, upsert_issue
from course_slide_factory.workflow import build_empty_state, generate_stage, update_setup_from_inputs


def _activity_value(update):
    return update["value"] if isinstance(update, dict) else update


def _update_visible(update):
    return update["visible"] if isinstance(update, dict) else None


def test_workflow_progress_summary_starts_empty():
    state = build_empty_state(deck_title="UI Test", source_url="mock://source")

    summary = workflow_progress_summary(state)
    rows = stage_score_rows(state, "setup_inputs")

    assert summary["complete"] == 0
    assert summary["total"] == len(STAGE_IDS)
    assert summary["percent"] == 0
    assert rows[0]["stage_id"] == "setup_inputs"
    assert rows[0]["active"]
    assert rows[0]["score"] is None
    assert rows[0]["status"] == "pending"


def test_stage_score_rows_track_complete_review_stale_and_blockers():
    state = build_empty_state(deck_title="UI Test", source_url="mock://source")
    state, _message = generate_stage(state, "setup_inputs")
    state.stages["setup_inputs"].score = 92
    approve_current_artifact(state, "setup_inputs")

    state, _message = generate_stage(state, "text_generation")
    state.stages["text_generation"].score = 72
    state.stages["title_generation"].is_stale = True
    upsert_issue(
        state,
        IssueType.TECHNICAL_INACCURACY,
        IssueSeverity.BLOCKER,
        "Blocking issue",
        stage_id="technical_review",
    )
    state.stages["technical_review"].is_stale = False

    summary = workflow_progress_summary(state)
    rows = {row["stage_id"]: row for row in stage_score_rows(state, "text_generation")}
    score_strip = render_stage_score_strip(state, "text_generation")

    assert summary["complete"] == 1
    assert summary["percent"] == round(1 / len(STAGE_IDS) * 100)
    assert summary["unresolved_blockers"] >= 1
    assert rows["setup_inputs"]["status"] == "complete"
    assert rows["text_generation"]["status"] == "review"
    assert rows["text_generation"]["active"]
    assert rows["title_generation"]["status"] == "stale"
    assert rows["technical_review"]["status"] == "blocked"
    assert "Text" in score_strip
    assert "72/100" in score_strip


def test_setup_stage_shows_feedback_workspaces():
    updates = workspace_visibility_updates("setup_inputs")

    assert [update["visible"] for update in updates] == [True, False, False, False, True]


def test_activity_messages_render_distinct_pending_and_complete_states():
    pending = pending_activity_message("Creating setup candidate...")
    complete = render_activity_message("Setup candidate created.", "complete")

    assert "activity-card pending" in pending
    assert "Working" in pending
    assert "Creating setup candidate..." in pending
    assert "activity-card complete" in complete
    assert "Done" in complete


def test_light_theme_css_covers_gradio_dataframes_and_action_hierarchy():
    assert ":root" in CSS
    assert "color-scheme: light" in CSS
    assert "--border-color-primary: #d9dee7" in CSS
    assert "--body-text-color-subdued: #4b5563" in CSS
    assert "--ag-background-color: #ffffff" in CSS
    assert ".compact-panel .ag-header" in CSS
    assert '.compact-panel [role="gridcell"]' in CSS
    assert ".compact-panel .virtual-row" in CSS
    assert ".compact-panel .body-cell" in CSS
    assert '.gradio-container [data-testid="dataframe"] tbody tr:nth-child(even)' in CSS
    assert '.gradio-container [role="gridcell"]' in CSS
    assert ".gradio-container .header-table" in CSS
    assert ".gradio-container .virtual-row.row-odd" in CSS
    assert ".production-action" in CSS
    assert ".approval-action" in CSS
    assert ".score-gate" in CSS
    assert '.score-gate [data-testid="column"]' in CSS


def test_stage_score_gate_panel_renders_pending_and_scored_states():
    state = build_empty_state(deck_title="UI Test", source_url="mock://source")

    pending = render_stage_score_gate_panel(state, "setup_inputs")
    state.stages["setup_inputs"].score = 91
    scored = render_stage_score_gate_panel(state, "setup_inputs")

    assert "Setup & Inputs score" in pending
    assert "Pending" in pending
    assert "stage-result-card scored" in scored
    assert "91/100" in scored


def test_setup_candidate_button_returns_visible_snapshot_feedback(tmp_path):
    instructions = tmp_path / "instructions.txt"
    instructions.write_text("Course plan\n- Explain the core course concept.", encoding="utf-8")

    generated = handle_generate(
        None,
        "Course Slide Deck",
        "",
        "mock://template/course",
        "",
        True,
        "obj_1: Explain the core course concept.",
        [str(instructions)],
        "",
        "",
        "instructions_file",
        None,
        "",
        False,
        "setup_inputs",
    )

    assert _update_visible(generated[1]) is True
    message = _activity_value(generated[2])
    assert "activity-card complete" in message
    assert "Done" in message
    assert "Setup candidate created as setup_inputs_v1." in message
    assert "1 parsed upload(s)" in message
    assert "Check Setup Score" in message
    assert "### Setup & Inputs output" in generated[3]
    assert "Input summary" in generated[3]
    assert "Parsed uploads: 1" in generated[3]
    assert "instructions.txt" in generated[3]
    assert '"input_summary"' not in generated[3]
    assert generated[4] == generated[3]


def test_readable_artifact_markdown_uses_human_summary_not_json(tmp_path):
    instructions = tmp_path / "instructions.txt"
    instructions.write_text("Course plan\n- Explain the core course concept.", encoding="utf-8")
    state = build_empty_state(deck_title="Course Slide Deck", source_url="")
    update_setup_from_inputs(
        state,
        deck_title="Course Slide Deck",
        source_url="",
        template_url="mock://template/course",
        output_folder_id="",
        dry_run=True,
        objectives_text="obj_1: Explain the core course concept.",
        material_files=[str(instructions)],
        start_mode="instructions_file",
    )
    state, _message = generate_stage(state, "setup_inputs")

    output = readable_current_artifact_markdown(state, "setup_inputs")

    assert output.startswith("### Setup & Inputs output")
    assert "Input summary" in output
    assert "Uploaded materials" in output
    assert '"uploaded_materials"' not in output


def test_stage_navigation_clears_activity_message():
    generated = handle_generate(
        None,
        "Course Slide Deck",
        "mock://source",
        "mock://template/course",
        "",
        True,
        "",
        [],
        "",
        "",
        "instructions_file",
        None,
        "",
        False,
        "setup_inputs",
    )

    navigated = handle_stage_nav(generated[0], "source_extraction_objective_mapping")

    assert navigated[4]["visible"] is False
    assert navigated[5] == ""


def test_setup_grade_button_path_returns_score_feedback():
    generated = handle_generate(
        None,
        "Course Slide Deck",
        "mock://source",
        "mock://template/course",
        "",
        True,
        "",
        [],
        "",
        "",
        "instructions_file",
        None,
        "",
        False,
        "setup_inputs",
    )

    graded = handle_grade(generated[0], "setup_inputs")

    message = _activity_value(graded[2])
    assert "activity-card complete" in message
    assert "Grade for Setup & Inputs:" in message