Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import json | |
| import pytest | |
| from course_slide_factory.fixtures import ( | |
| constraint_violation_change_set_job, | |
| non_waivable_blocker_job, | |
| proposed_change_set_job, | |
| restore_candidate_version_job, | |
| review_packet_job, | |
| review_queue_mixed_issues_job, | |
| role_filtered_review_job, | |
| semantic_diff_changed_slide_job, | |
| selected_slide_improvement_job, | |
| waivable_major_issue_job, | |
| ) | |
| from course_slide_factory.models import ArtifactStatus, IssueSeverity, IssueStatus, IssueType, ReviewRole, RevisionConstraints | |
| from course_slide_factory.quality import can_unlock_next_stage, get_current_stage_artifact, has_valid_human_approval, upsert_issue | |
| from course_slide_factory.review import ( | |
| apply_proposed_change_set, | |
| build_review_queue, | |
| compare_artifact_versions_semantically, | |
| compute_reviewer_productivity_metrics, | |
| create_review_packet, | |
| generate_suggested_fix_for_issue, | |
| generate_suggested_fixes, | |
| improve_selected_slides, | |
| reject_proposed_change_set, | |
| restore_artifact_version_as_candidate, | |
| update_issue_status, | |
| validate_change_set_against_constraints, | |
| waive_issue, | |
| ) | |
| from course_slide_factory.workflow import ( | |
| build_empty_state, | |
| run_draft_until_next_required_approval, | |
| run_generate_and_grade_for_current_stage, | |
| ) | |
| def _first_issue_id(state, issue_type: IssueType | None = None, severity: IssueSeverity | None = None) -> str: | |
| for issue in state.issues.values(): | |
| if issue_type and issue.issue_type != issue_type: | |
| continue | |
| if severity and issue.severity != severity: | |
| continue | |
| return issue.issue_id | |
| raise AssertionError("Expected fixture issue") | |
| def test_review_queue_filters_sorting_and_role_visibility(): | |
| state = role_filtered_review_job() | |
| queue = build_review_queue(state) | |
| assert queue[0].severity == IssueSeverity.BLOCKER | |
| assert any(item.status == IssueStatus.OPEN for item in queue) | |
| visual_queue = build_review_queue(state, role=ReviewRole.VISUAL_DESIGNER) | |
| assert any(item.issue_type == IssueType.LAYOUT_SCHEMA_INVALID for item in visual_queue) | |
| assert any(item.severity == IssueSeverity.BLOCKER for item in visual_queue) | |
| stage_queue = build_review_queue(state, stage_id="text_generation") | |
| assert stage_queue | |
| assert all(item.stage_id == "text_generation" for item in stage_queue) | |
| issue_id = _first_issue_id(state, IssueType.TEXT_DENSITY_EXCEEDED) | |
| update_issue_status(issue_id, IssueStatus.ACKNOWLEDGED, state, note="Seen") | |
| acknowledged = build_review_queue(state, status=IssueStatus.ACKNOWLEDGED) | |
| assert [item.issue_id for item in acknowledged] == [issue_id] | |
| def test_issue_lifecycle_waiver_and_wont_fix_rules(tmp_path, monkeypatch): | |
| state = waivable_major_issue_job() | |
| issue_id = _first_issue_id(state) | |
| update_issue_status(issue_id, IssueStatus.ACKNOWLEDGED, state, note="Seen") | |
| assert state.issues[issue_id].status == IssueStatus.ACKNOWLEDGED | |
| update_issue_status(issue_id, IssueStatus.IN_PROGRESS, state, note="Fixing") | |
| assert state.issues[issue_id].status == IssueStatus.IN_PROGRESS | |
| update_issue_status(issue_id, IssueStatus.RESOLVED, state, note="Rechecked manually") | |
| assert state.issues[issue_id].resolved | |
| state = waivable_major_issue_job() | |
| issue_id = _first_issue_id(state) | |
| waive_issue(issue_id, "Acceptable for pilot", state) | |
| assert state.issues[issue_id].status == IssueStatus.WAIVED | |
| blocker_state = non_waivable_blocker_job() | |
| with pytest.raises(ValueError, match="cannot be waived"): | |
| waive_issue(_first_issue_id(blocker_state), "No override", blocker_state) | |
| state = waivable_major_issue_job() | |
| with pytest.raises(ValueError, match="requires a reason"): | |
| update_issue_status(_first_issue_id(state), IssueStatus.WONT_FIX, state) | |
| packet_state = review_packet_job() | |
| monkeypatch.chdir(tmp_path) | |
| packet = create_review_packet(packet_state, format="markdown") | |
| assert "Waived Issues" in (tmp_path / packet.path).read_text(encoding="utf-8") | |
| def test_suggested_fixes_cover_core_issue_types(): | |
| state = review_queue_mixed_issues_job() | |
| notes_issue = upsert_issue( | |
| state, | |
| IssueType.SPEAKER_NOTES_MISSING, | |
| IssueSeverity.MAJOR, | |
| "Missing notes", | |
| stage_id="text_generation", | |
| slide_id="slide_2", | |
| ) | |
| layout_issue = upsert_issue( | |
| state, | |
| IssueType.LAYOUT_SCHEMA_INVALID, | |
| IssueSeverity.BLOCKER, | |
| "Bad layout", | |
| stage_id="aesthetic_ordering_visual_composition", | |
| slide_id="slide_1", | |
| ) | |
| fixes = generate_suggested_fixes(state) | |
| fix_types = {fix.fix_type for fix in fixes} | |
| assert "support_claim" in fix_types | |
| assert "reduce_text" in fix_types | |
| assert generate_suggested_fix_for_issue(notes_issue.issue_id, state).fix_type == "add_speaker_notes" | |
| assert generate_suggested_fix_for_issue(layout_issue.issue_id, state).fix_type == "change_layout" | |
| def test_critique_change_set_apply_and_reject_lifecycle(): | |
| state = proposed_change_set_job() | |
| change_set_id = next(iter(state.proposed_change_sets)) | |
| old_artifact = get_current_stage_artifact("text_generation", state) | |
| old_artifact_count = len(state.artifacts) | |
| apply_proposed_change_set(change_set_id, state) | |
| assert len(state.artifacts) == old_artifact_count + 1 | |
| assert get_current_stage_artifact("text_generation", state).artifact_version_id != old_artifact.artifact_version_id | |
| assert not has_valid_human_approval("text_generation", state) | |
| assert state.stages["image_visual_asset_generation"].is_stale | |
| assert any(event.event_type == "proposed_change_set_applied" for event in state.audit_events) | |
| rejected = proposed_change_set_job() | |
| change_set_id = next(iter(rejected.proposed_change_sets)) | |
| before = len(rejected.artifacts) | |
| reject_proposed_change_set(change_set_id, rejected, reason="Not needed") | |
| assert len(rejected.artifacts) == before | |
| assert rejected.proposed_change_sets[change_set_id].status == "rejected" | |
| def test_revision_constraints_block_violating_changes(): | |
| state = constraint_violation_change_set_job() | |
| change_set = state.proposed_change_sets["changes_constraint_violation"] | |
| violations = validate_change_set_against_constraints(change_set, change_set.constraints) | |
| assert violations | |
| assert "preserve_slide_titles" in violations[0].message | |
| original_title = state.slides["slide_1"].title | |
| before = len(state.artifacts) | |
| apply_proposed_change_set(change_set.change_set_id, state) | |
| assert state.slides["slide_1"].title == original_title | |
| assert len(state.artifacts) == before | |
| assert state.proposed_change_sets[change_set.change_set_id].status == "rejected" | |
| def test_targeted_slide_improvement_affects_only_selected_slide(): | |
| state = selected_slide_improvement_job() | |
| slide_2_text = state.slides["slide_2"].visible_text | |
| change_set = improve_selected_slides( | |
| "text_generation", | |
| ["slide_1"], | |
| state, | |
| constraints=RevisionConstraints(selected_slide_ids=["slide_1"]), | |
| action="rewrite_visible_text", | |
| ) | |
| assert change_set.changes | |
| assert all(change.target_id == "slide_1" for change in change_set.changes) | |
| apply_proposed_change_set(change_set.change_set_id, state) | |
| assert "Review draft" in state.slides["slide_1"].visible_text | |
| assert state.slides["slide_2"].visible_text == slide_2_text | |
| def test_fast_path_generates_grades_but_does_not_approve_or_unlock(): | |
| state = build_empty_state( | |
| deck_title="Fast Path", | |
| source_url="mock://source", | |
| template_url="mock://template", | |
| ) | |
| run_generate_and_grade_for_current_stage("setup_inputs", state) | |
| assert get_current_stage_artifact("setup_inputs", state).status == ArtifactStatus.CANDIDATE | |
| assert state.stages["setup_inputs"].score is not None | |
| assert not has_valid_human_approval("setup_inputs", state) | |
| assert not can_unlock_next_stage("setup_inputs", state) | |
| assert any(event.event_type == "fast_path_generate_grade_stopped" for event in state.audit_events) | |
| blocker_state = non_waivable_blocker_job() | |
| run_draft_until_next_required_approval("technical_review", blocker_state) | |
| assert any( | |
| event.event_type == "fast_path_generate_grade_stopped" and event.stage_id == "technical_review" | |
| for event in blocker_state.audit_events | |
| ) | |
| def test_semantic_diff_detects_slide_level_changes(): | |
| state = semantic_diff_changed_slide_job() | |
| versions = state.stage_artifact_versions["text_generation"][-2:] | |
| comparison = compare_artifact_versions_semantically(versions[0], versions[1], state) | |
| assert "slide_1" in comparison.changed_titles | |
| assert "slide_1" in comparison.changed_slide_ids | |
| assert comparison.diff_text | |
| def test_review_packet_markdown_json_and_role_filter(tmp_path, monkeypatch): | |
| state = review_packet_job() | |
| monkeypatch.chdir(tmp_path) | |
| markdown_packet = create_review_packet(state, role=ReviewRole.SME, format="markdown") | |
| json_packet = create_review_packet(state, role=ReviewRole.SME, format="json") | |
| markdown_text = (tmp_path / markdown_packet.path).read_text(encoding="utf-8") | |
| json_payload = json.loads((tmp_path / json_packet.path).read_text(encoding="utf-8")) | |
| assert "Open Blockers" in markdown_text | |
| assert "Suggested Fixes" in markdown_text | |
| assert "Objective Coverage Summary" in markdown_text | |
| assert any(issue["severity"] == "blocker" for issue in json_payload["issues"]) | |
| assert json_payload["role"] == ReviewRole.SME.value | |
| def test_candidate_restore_rules(): | |
| state = restore_candidate_version_job() | |
| candidate_id = next( | |
| artifact.artifact_version_id | |
| for artifact in state.artifacts.values() | |
| if artifact.stage_id == "text_generation" | |
| and artifact.status == ArtifactStatus.CANDIDATE | |
| and not artifact.is_current | |
| ) | |
| restore_artifact_version_as_candidate(candidate_id, state) | |
| current = get_current_stage_artifact("text_generation", state) | |
| assert current.parent_artifact_version_ids == [candidate_id] | |
| assert current.status == ArtifactStatus.CANDIDATE | |
| assert not has_valid_human_approval("text_generation", state) | |
| assert state.stages["image_visual_asset_generation"].is_stale | |
| for status in [ArtifactStatus.STALE, ArtifactStatus.INVALIDATED, ArtifactStatus.EXPORTED]: | |
| blocked = restore_candidate_version_job() | |
| candidate_id = next( | |
| artifact.artifact_version_id | |
| for artifact in blocked.artifacts.values() | |
| if artifact.stage_id == "text_generation" | |
| and artifact.status == ArtifactStatus.CANDIDATE | |
| and not artifact.is_current | |
| ) | |
| blocked.artifacts[candidate_id].status = status | |
| with pytest.raises(ValueError): | |
| restore_artifact_version_as_candidate(candidate_id, blocked) | |
| def test_reviewer_productivity_metrics(): | |
| state = review_queue_mixed_issues_job() | |
| metrics = compute_reviewer_productivity_metrics(state) | |
| assert metrics.total_issues >= 3 | |
| assert metrics.blocker_count >= 1 | |
| assert "technical_review" in metrics.issues_by_stage | |