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