"""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"