"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21/22) + REQ-273..275 (v1.23): S&P theme + slide render pipeline + deck-refinement tests. v1.20 validates: - The mermaid theme JSON contains the S&P colors - Every .mmd has a corresponding .png - The render_slides.sh script exists and is executable - The CI workflow file exists v1.21/22 adds (REQ-245,251,252): - Deck renamed to nova-autonomous-cloud-delivery* - No maturity badges in the Marp deck - No version in the Marp footer/title slide - 20 main + 1 appendix slides - No D-###/REQ-###/internal .py paths in audience-facing slides v1.23 (REQ-273,274,275) — single-document + dual-PPTX + image-inlining pipeline: - The plain `.md` is gone; `*-marp.md` is the sole source of truth. - Marp deck uses `theme: default` + an inline `style:` block (S&P colors). - `nova-sp-theme.css` is RETAINED AS REFERENCE (not loaded at render). - HTML has base64-inlined images (zero `src="assets/` references). - A second PPTX (`*-python.pptx`) is produced by `scripts/render_pptx.py`. - Speaker notes live as `` HTML comments. - Benefit callouts use `
` (no `**Benefit:**` prefixes). - The purged term "penetrate" is absent repo-wide. """ import re from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent PRESENTATIONS = ROOT / "docs" / "presentations" ASSETS = PRESENTATIONS / "assets" THEME_CSS = ASSETS / "nova-sp-theme.css" THEME_JSON = ASSETS / "mmd" / "sp-theme.json" MARP_DECK = PRESENTATIONS / "nova-autonomous-cloud-delivery-marp.md" SOURCE_MD = PRESENTATIONS / "nova-autonomous-cloud-delivery.md" HTML = PRESENTATIONS / "nova-autonomous-cloud-delivery.html" PYTHON_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery-python.pptx" MARP_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery.pptx" RENDER_SCRIPT = ROOT / "scripts" / "render_slides.sh" SLIDES_WORKFLOW = ROOT / ".github" / "workflows" / "slides.yml" def _frontmatter(text: str) -> str: """Return the Marp frontmatter block (between the first two `---`).""" fm_match = re.match(r'^---\n(.*?)\n---', text, re.DOTALL) assert fm_match, "Marp frontmatter not found" return fm_match.group(1) # --- S&P theme reference + mermaid theme ------------------------------- def test_sp_theme_css_exists(): """REQ-239: nova-sp-theme.css exists (retained as a reference).""" assert THEME_CSS.is_file(), f"theme CSS not found: {THEME_CSS}" def test_nova_sp_theme_css_retained_as_reference(): """REQ-274: nova-sp-theme.css is retained as a REFERENCE only and is explicitly NOT loaded at render time (the live styling is the inline `style:` block in the -marp.md frontmatter).""" assert THEME_CSS.is_file(), f"theme CSS not found: {THEME_CSS}" css = THEME_CSS.read_text() assert "not loaded at render" in css.lower(), \ "nova-sp-theme.css does not document itself as 'not loaded at render'" def test_sp_theme_css_has_snp_colors(): """REQ-239: the reference CSS still carries S&P Red and Black.""" css = THEME_CSS.read_text() assert "#D6002A" in css, "S&P Red (#D6002A) missing from theme CSS" assert "#1B1B1B" in css, "S&P Black (#1B1B1B) missing from theme CSS" def test_sp_theme_json_has_snp_colors(): """The mermaid theme JSON has S&P colors (mermaid PNGs are S&P-themed).""" json_text = THEME_JSON.read_text() assert "#D6002A" in json_text, "S&P Red missing from mermaid theme" assert "#1B1B1B" in json_text, "S&P Black missing from mermaid theme" # --- Marp deck: theme + inline style ---------------------------------- def test_marp_deck_uses_default_theme(): """REQ-274: the Marp deck frontmatter uses `theme: default` (not the retired `theme: nova-sp`). S&P styling is delivered by the inline `style:` block, not the standalone CSS.""" frontmatter = _frontmatter(MARP_DECK.read_text()) assert re.search(r"^theme:\s*default\s*$", frontmatter, re.MULTILINE), \ "Marp deck does not set `theme: default` in the frontmatter" assert "nova-sp" not in frontmatter, \ "Marp deck still references the retired `nova-sp` theme" def test_marp_deck_has_sp_inline_style(): """REQ-274: the inline `style:` block carries the S&P properties (#D6002A, #1B1B1B, and the `section.title` rule).""" frontmatter = _frontmatter(MARP_DECK.read_text()) assert "style:" in frontmatter, "frontmatter has no inline `style:` block" # The inline style block extends past the frontmatter close in Marp # (the `style:` value is a multi-line YAML literal). Read the whole # deck so we capture the full style block. deck = MARP_DECK.read_text() assert "#D6002A" in deck, "inline style: block missing #D6002A" assert "#1B1B1B" in deck, "inline style: block missing #1B1B1B" assert "section.title" in deck, \ "inline style: block missing the `section.title` rule" def test_marp_deck_no_badges(): """REQ-252: no maturity badges in the Marp deck.""" text = MARP_DECK.read_text() assert "badge" not in text, "Marp deck still contains badge spans" def test_marp_deck_no_version_in_footer(): """REQ-251: no version (v1.x) in the Marp frontmatter footer/header.""" frontmatter = _frontmatter(MARP_DECK.read_text()) assert not re.search(r"v1\.\d+", frontmatter), \ f"Marp frontmatter still contains a version: {frontmatter}" assert "Act %" not in frontmatter, \ "Marp frontmatter still contains 'Act %{page}' artifact" def test_marp_deck_title_slide_no_version_subtitle(): """REQ-251: the title slide does not carry a version subtitle.""" text = MARP_DECK.read_text() after_fm = text.split("---\n", 2)[2] if text.startswith("---") else text first_slide = after_fm.split("\n---\n")[0] assert "v1.18" not in first_slide, \ "Title slide still contains 'v1.18' subtitle" assert "Citizen Developer & Production-Grade Guidance" not in first_slide, \ "Title slide still contains the old version subtitle" def test_marp_deck_title_is_autonomous_cloud_delivery(): """REQ-245: the deck title is 'Nova — The Autonomous Cloud Delivery Platform'.""" text = MARP_DECK.read_text() assert "Autonomous Cloud Delivery Platform" in text, \ "Deck title is not 'Autonomous Cloud Delivery Platform'" assert "No-Humans Infrastructure Platform" not in text, \ "Deck still carries the old 'No-Humans Infrastructure Platform' title" def test_marp_deck_slide_count(): """REQ-245/261: 20 main slides + 1 appendix = 21 slide sections (22 rendered sections incl. the H1 title slide).""" text = MARP_DECK.read_text() main_slides = re.findall(r"^## Slide ", text, re.MULTILINE) appendix_slides = re.findall(r"^## Appendix ", text, re.MULTILINE) assert len(main_slides) == 20, \ f"expected 20 main slides, found {len(main_slides)}" assert len(appendix_slides) == 1, \ f"expected 1 appendix slide, found {len(appendix_slides)}" def test_marp_deck_no_internal_citations(): """REQ-252: no D-### decision IDs, REQ-### requirement IDs, or internal .py file paths in the audience-facing Marp deck SLIDE BODIES. Internal provenance is allowed inside `` HTML comments (speaker notes / talking points), which Marp excludes from the rendered slide.""" text = MARP_DECK.read_text() # Strip HTML comments (speaker notes + talking points) before checking. body = re.sub(r"", "", text, flags=re.DOTALL) assert not re.search(r"\bD-\d{3}\b", body), \ "Marp deck slide body contains D-### decision IDs" assert not re.search(r"\bREQ-\d{3}\b", body), \ "Marp deck slide body contains REQ-### requirement IDs" assert not re.search(r"\b(outbox_writer|confidence_signal|hitl_gates|" r"attestation_matrix|checkov_adapter|infracost_adapter|" r"contract_resolver|run_platform)\.py\b", body), \ "Marp deck slide body contains internal .py file paths" # --- Speaker notes + benefit callouts (REQ-274) ---------------------- def test_speaker_notes_as_html_comments(): """REQ-274: speaker notes are embedded as `` HTML comments (Marp excludes HTML comments from the rendered slide; the comments are for authors/presenters). Expect >= 20 (one per main slide) + the appendix slide.""" text = MARP_DECK.read_text() count = len(re.findall(r"