using System.Text.Json; using AxCopilot.Services.Agent; using FluentAssertions; using Xunit; namespace AxCopilot.Tests.Services; public class DeckQualityReviewServiceTests { [Fact] public void ReviewDeck_ShouldFlagMissingExecutiveSummaryAndRecommendation() { using var slides = JsonDocument.Parse( """ [ { "layout": "content", "title": "Current State", "body": "Point 1\nPoint 2\nPoint 3\nPoint 4\nPoint 5\nPoint 6\nPoint 7" }, { "layout": "content", "title": "Findings", "body": "Observation A\nObservation B\nObservation C" } ] """); var review = DeckQualityReviewService.ReviewDeck("Weak Deck", slides.RootElement, hasTemplate: false, autoRepairCount: 0); review.Score.Should().BeLessThan(75); review.Issues.Should().Contain(issue => issue.Message.Contains("Executive Summary")); review.Issues.Should().Contain(issue => issue.Message.Contains("Recommendation")); } [Fact] public void ReviewDeck_ShouldRewardBalancedConsultingDeck() { using var slides = JsonDocument.Parse( """ [ { "layout": "title", "title": "Growth Strategy" }, { "layout": "executive_summary", "title": "Executive Summary", "headline": "Headline", "summary_points": ["A","B","C"], "recommendation": "Do B" }, { "layout": "comparison", "title": "Options", "headline": "Compare", "options": [{ "name": "A", "pros": "Fast", "cons": "Shallow", "verdict": "Fastest" }, { "name": "B", "pros": "Balanced", "cons": "Needs alignment", "verdict": "Recommended" }] }, { "layout": "roadmap", "title": "Roadmap", "headline": "Execute in three phases", "phases": [{ "title": "Design", "detail": "Lock scope" }, { "title": "Launch", "detail": "Go live" }] }, { "layout": "recommendation", "title": "Recommendation", "recommendation": "Approve phase-1", "summary_points": ["Reason 1", "Reason 2"] }, { "layout": "table", "title": "Appendix & Evidence", "headers": ["Evidence","Detail"], "rows": [["Metric","Value"]] } ] """); var review = DeckQualityReviewService.ReviewDeck("Strong Deck", slides.RootElement, hasTemplate: true, autoRepairCount: 2, storyline: ["Executive Summary", "Options", "Roadmap"]); review.Score.Should().BeGreaterThan(80); review.Strengths.Should().Contain(strength => strength.Contains("Executive Summary")); review.Issues.Should().NotContain(issue => issue.Severity == DeckReviewSeverity.Critical); } [Fact] public void ReviewDeck_ShouldSurfaceSlideLevelQualityAlerts() { using var slides = JsonDocument.Parse( """ [ { "layout": "title", "title": "Growth Strategy" }, { "layout": "executive_summary", "title": "Executive Summary", "headline": "This headline is intentionally very long so that it exceeds the preferred consulting slide headline length and triggers the slide quality gate.", "summary_points": ["Only one point"] }, { "layout": "comparison", "title": "Options", "headline": "Compare options", "options": [{ "name": "A", "pros": "Fast", "cons": "Shallow", "verdict": "Only option" }] } ] """); var review = DeckQualityReviewService.ReviewDeck("Alert Deck", slides.RootElement, hasTemplate: false, autoRepairCount: 0); review.Issues.Should().Contain(issue => issue.Message.Contains("Slide 2")); review.Issues.Should().Contain(issue => issue.Message.Contains("headline is too long")); review.Issues.Should().Contain(issue => issue.Message.Contains("decision ask", StringComparison.OrdinalIgnoreCase)); review.Issues.Should().Contain(issue => issue.Message.Contains("comparison slide needs at least two options")); } [Fact] public void ReviewDeck_ShouldFlagMissingStorylineSlides_WhenHintsAreProvided() { using var slides = JsonDocument.Parse( """ [ { "layout": "title", "title": "PMO Steering" }, { "layout": "executive_summary", "title": "Executive Summary", "headline": "Message", "summary_points": ["A", "B"], "recommendation": "Proceed" }, { "layout": "recommendation", "title": "Recommendation", "recommendation": "Proceed", "summary_points": ["Reason"] } ] """); var review = DeckQualityReviewService.ReviewDeck( "Storyline Deck", slides.RootElement, hasTemplate: false, autoRepairCount: 0, storyline: ["Executive Summary", "Options", "Roadmap", "Appendix"]); review.Issues.Should().Contain(issue => issue.Message.Contains("Storyline expects an options or comparison slide", StringComparison.OrdinalIgnoreCase)); review.Issues.Should().Contain(issue => issue.Message.Contains("Storyline expects a roadmap slide", StringComparison.OrdinalIgnoreCase)); review.Issues.Should().Contain(issue => issue.Message.Contains("Storyline expects appendix or evidence support", StringComparison.OrdinalIgnoreCase)); } [Fact] public void ReviewDeck_ShouldFlagRecommendationSlideWithoutRationaleOrNextSteps() { using var slides = JsonDocument.Parse( """ [ { "layout": "title", "title": "PMO Steering" }, { "layout": "executive_summary", "title": "Executive Summary", "headline": "Delivery is stable", "summary_points": ["A", "B"], "recommendation": "Proceed" }, { "layout": "recommendation", "title": "Recommendation", "recommendation": "Proceed" } ] """); var review = DeckQualityReviewService.ReviewDeck("Thin Recommendation", slides.RootElement, hasTemplate: false, autoRepairCount: 0); review.Issues.Should().Contain(issue => issue.Message.Contains("supporting rationale or next steps", StringComparison.OrdinalIgnoreCase)); } [Fact] public void ReviewDeck_ShouldFlagComparisonTradeoffsAndKpiContext() { using var slides = JsonDocument.Parse( """ [ { "layout": "title", "title": "Board Review" }, { "layout": "executive_summary", "title": "Executive Summary", "summary_points": ["A", "B"], "recommendation": "Proceed" }, { "layout": "comparison", "title": "Options", "headline": "Choose a path", "options": [ { "name": "A", "verdict": "Recommended" }, { "name": "B", "verdict": "Fallback" } ] }, { "layout": "kpi_dashboard", "title": "KPIs", "headline": "Performance snapshot", "kpis": [ { "label": "Margin", "value": "+2.4pt" }, { "label": "Lead Time", "value": "-12%" }, { "label": "Quality", "value": "96%" } ] } ] """); var review = DeckQualityReviewService.ReviewDeck("Thin Signals", slides.RootElement, hasTemplate: false, autoRepairCount: 0); review.Issues.Should().Contain(issue => issue.Message.Contains("sharper single-message headline", StringComparison.OrdinalIgnoreCase)); review.Issues.Should().Contain(issue => issue.Message.Contains("trade-offs behind the verdict", StringComparison.OrdinalIgnoreCase)); review.Issues.Should().Contain(issue => issue.Message.Contains("trend or note context", StringComparison.OrdinalIgnoreCase)); } }