Slide-creation-engine / tests /test_p1_review.py
rigelbar's picture
Implement P1 review productivity layer
2b5291f
Raw
History Blame Contribute Delete
11.2 kB
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