test(P5): ci + tests + readme for single-doc dual-pptx pipeline (REQ-273,274,275)

CI workflows: install python-pptx, pin CLI versions, stage both PPTX +
inlined HTML. test_slides_pipeline.py: inverted theme assertion (now
default+inline), deleted source-md tests, added 8 new tests
(penetrate absence, image inlining, python-pptx, benefit class, single
source, speaker-notes comments, default theme, css retained). New
test_pptx_generator.py: slide count, title colors, slide titles, table
rendering, image embedding, benefit callout. README rewritten for 3-step
single-document + dual-PPTX + image-inlining pipeline.

---ci---
project: acdl
phase: 5
milestone: v1.23
status: execute
phase_role: execution
---/ci---
This commit is contained in:
Jon Chery
2026-08-12 00:34:23 +00:00
parent 9b8ab75b85
commit e07a210c70
6 changed files with 726 additions and 406 deletions
+145
View File
@@ -0,0 +1,145 @@
"""REQ-274: tests for `scripts/render_pptx.py` — the structured, editable
python-pptx deck produced alongside the MARP-rendered PPTX.
The python-pptx deck is a native OOXML presentation: real text boxes,
native tables, embedded pictures, and italic benefit callouts. These
tests are offline (no AWS, no network) and assert the structural
properties of the committed `*-python.pptx` artifact.
"""
from pathlib import Path
from typing import cast
import pytest
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.presentation import Presentation as PresentationT
from pptx.shapes.autoshape import Shape
ROOT = Path(__file__).resolve().parent.parent
PRESENTATIONS = ROOT / "docs" / "presentations"
MARPT_DECK = PRESENTATIONS / "nova-autonomous-cloud-delivery-marp.md"
PYTHON_PPTX = PRESENTATIONS / "nova-autonomous-cloud-delivery-python.pptx"
# Title slide + 20 main slides + 1 appendix slide.
EXPECTED_SLIDE_COUNT = 22
@pytest.fixture(scope="module")
def prs() -> PresentationT:
"""Load the committed python-pptx deck once for the whole module."""
assert PYTHON_PPTX.is_file(), f"python-pptx PPTX not found: {PYTHON_PPTX}"
return Presentation(str(PYTHON_PPTX))
def _slide_titles(prs: PresentationT) -> list[str]:
"""Return the first non-empty text-frame line per slide (the title)."""
titles: list[str] = []
for slide in prs.slides:
for shape in slide.shapes:
if not shape.has_text_frame:
continue
text = cast(Shape, shape).text_frame.text.strip()
if not text:
continue
# The title is the first non-empty line of the first non-empty
# text frame we find on the slide.
first_line = text.split("\n")[0].strip()
if first_line:
titles.append(first_line)
break
else:
titles.append("")
return titles
def test_slide_count(prs: PresentationT):
"""REQ-269/274: the python-pptx deck has 22 slides
(title + 20 main + 1 appendix)."""
assert len(prs.slides) == EXPECTED_SLIDE_COUNT, \
f"expected {EXPECTED_SLIDE_COUNT} slides, got {len(prs.slides)}"
def test_title_slide_colors(prs: PresentationT):
"""REQ-269: the title slide (slide 0) has a solid-filled background
shape carrying the S&P Red (#D6002A) brand color (the title slide is
a red-bar-on-black layout)."""
title_slide = prs.slides[0]
red_found = False
black_found = False
for shape in title_slide.shapes:
fill = getattr(shape, "fill", None)
if fill is None:
continue
try:
if fill.type != 1: # MSO_FILL.SOLID
continue
except Exception:
continue
rgb = str(fill.fore_color.rgb).upper()
if rgb == "D6002A":
red_found = True
if rgb == "1B1B1B":
black_found = True
assert red_found, \
"title slide has no solid-fill shape with S&P Red (#D6002A)"
def test_expected_slide_titles(prs: PresentationT):
"""REQ-269/274: spot-check that key slide titles match the markdown
deck (The Problem, Nova's Vision, Recap + Ask)."""
titles = _slide_titles(prs)
# Build a flat lowercase concatenation for substring checks.
flat = " | ".join(titles).lower()
expected = [
"the problem",
"nova's vision",
"recap + ask",
]
missing = [t for t in expected if t not in flat]
assert not missing, \
f"missing expected slide titles in python-pptx deck: {missing}; " \
f"found titles: {titles}"
def test_table_rendering(prs: PresentationT):
"""REQ-269: a slide with a table (the RACI slide) has a native PPTX
table shape (GraphicFrame with has_table=True)."""
table_slides = []
for idx, slide in enumerate(prs.slides):
for shape in slide.shapes:
if shape.shape_type == MSO_SHAPE_TYPE.TABLE or getattr(
shape, "has_table", False
):
table_slides.append(idx)
break
assert table_slides, \
"no slide in the python-pptx deck has a native PPTX table shape"
def test_image_embedding(prs: PresentationT):
"""REQ-269: a slide with an image (the Platform Pipeline slide)
has a native PPTX picture shape."""
picture_slides = []
for idx, slide in enumerate(prs.slides):
for shape in slide.shapes:
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
picture_slides.append(idx)
break
assert picture_slides, \
"no slide in the python-pptx deck has a native PPTX picture shape"
def test_benefit_callout_present(prs: PresentationT):
"""REQ-269/274: at least one slide has an italic text run (the
benefit callout, rendered as italic body text by render_pptx.py)."""
italic_runs = 0
for slide in prs.slides:
for shape in slide.shapes:
if not shape.has_text_frame:
continue
for paragraph in cast(Shape, shape).text_frame.paragraphs:
for run in paragraph.runs:
if run.font.italic and run.text.strip():
italic_runs += 1
assert italic_runs > 0, \
"no italic text runs found in the python-pptx deck (benefit callout)"
+350 -243
View File
@@ -1,21 +1,28 @@
"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21): S&P theme + slide render
pipeline + deck-refinement tests.
"""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 Marp deck frontmatter references nova-sp-theme.css
- The CSS file contains the S&P colors (#D6002A, #1B1B1B)
- 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 adds (REQ-245,251,252):
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 (v1.22 split slides 3+8 to relieve overflow)
- 20 main + 1 appendix slides
- No D-###/REQ-###/internal .py paths in audience-facing slides
- Title is "Nova — The Autonomous Cloud Delivery Platform"
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 `<!-- Speaker notes: ... -->` HTML comments.
- Benefit callouts use `<div class="benefit">` (no `**Benefit:**` prefixes).
- The purged term "penetrate" is absent repo-wide.
"""
import re
from pathlib import Path
@@ -29,50 +36,216 @@ 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."""
"""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: CSS contains S&P Red and Black."""
"""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 also has S&P 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"
def test_marp_deck_uses_sp_theme():
"""REQ-239: Marp deck frontmatter references nova-sp-theme.css."""
# --- 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()
# The frontmatter is between the first two ---
fm_match = re.match(r'^---\n(.*?)\n---', text, re.DOTALL)
assert fm_match, "Marp frontmatter not found"
frontmatter = fm_match.group(1)
assert "nova-sp" in frontmatter, \
"Marp deck does not reference nova-sp theme"
assert "badge" not in text, "Marp deck still contains badge spans"
def test_marp_deck_not_using_default_theme():
"""The Marp deck must not use 'theme: default'."""
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()
fm_match = re.match(r'^---\n(.*?)\n---', text, re.DOTALL)
assert fm_match, "Marp frontmatter not found"
frontmatter = fm_match.group(1)
assert "theme: default" not in frontmatter, \
"Marp deck still uses 'theme: default' — should use nova-sp-theme.css"
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 `<!-- Speaker notes: ... -->`
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"<!-- Speaker notes:", text))
assert count >= 20, \
f"expected >=20 `<!-- Speaker notes:` comments, found {count}"
def test_benefit_callouts_use_class():
"""REQ-274: benefit callouts use `<div class="benefit">` (>= 21
occurrences — one per slide section incl. the title slide) and zero
`**Benefit:**` text prefixes."""
text = MARP_DECK.read_text()
class_count = text.count('class="benefit"')
assert class_count >= 21, \
f"expected >=21 `class=\"benefit\"` callouts, found {class_count}"
assert "**Benefit:**" not in text, \
"Marp deck still uses the retired `**Benefit:**` prefix"
# --- Single source of truth (REQ-274) --------------------------------
def test_single_source_of_truth():
"""REQ-274: the plain `nova-autonomous-cloud-delivery.md` is deleted;
`nova-autonomous-cloud-delivery-marp.md` is the sole source of truth."""
assert not SOURCE_MD.exists(), \
f"plain source markdown still exists (should be deleted): {SOURCE_MD}"
assert MARP_DECK.is_file(), \
f"Marp deck (sole source of truth) not found: {MARP_DECK}"
# --- Purged term (REQ-274) -------------------------------------------
def test_no_purged_loaded_term():
"""REQ-274: the purged term 'penetrate' (case-insensitive, any
inflection: penetrate, penetrating, penetration, ...) is absent
from docs/, .ciagent/PROJECT.md, and .ciagent/CLARIFY.md."""
targets = [
ROOT / "docs",
ROOT / ".ciagent" / "PROJECT.md",
ROOT / ".ciagent" / "CLARIFY.md",
]
hits = []
for target in targets:
if target.is_dir():
for path in target.rglob("*"):
if not path.is_file():
continue
if path.suffix in {".png", ".pptx", ".html", ".zip", ".json"}:
continue
try:
if "penetrat" in path.read_text().lower():
hits.append(str(path))
except (UnicodeDecodeError, OSError):
continue
elif target.is_file():
try:
if "penetrat" in target.read_text().lower():
hits.append(str(target))
except (UnicodeDecodeError, OSError):
hits.append(f"<unreadable {target}>")
assert not hits, \
f"purged term 'penetrate' still present in: {hits}"
# --- Render script ---------------------------------------------------
def test_render_slides_script_exists():
"""REQ-240: render_slides.sh exists and is executable."""
assert RENDER_SCRIPT.is_file(), "render_slides.sh not found"
@@ -104,6 +277,52 @@ def test_render_slides_default_deck_renamed():
"render_slides.sh does not default to nova-autonomous-cloud-delivery"
def test_render_slides_has_2x_scale():
"""REQ-258: render_slides.sh uses -s 2 (2x scale) and -b transparent."""
text = RENDER_SCRIPT.read_text()
assert "-s 2" in text, "render_slides.sh does not use -s 2 (2x scale)"
assert "-b transparent" in text, \
"render_slides.sh does not use -b transparent"
def test_render_slides_pins_cli_versions():
"""REQ-257: render_slides.sh pins marp-cli and mermaid-cli versions
(no @latest). REQ-273: pyproject.toml declares python-pptx in the
`slides` optional-dependency group."""
text = RENDER_SCRIPT.read_text()
assert "marp-cli@" in text, "render_slides.sh does not pin marp-cli"
assert "mermaid-cli@" in text, \
"render_slides.sh does not pin mermaid-cli"
assert "@latest" not in text, \
"render_slides.sh still uses @latest (not pinned)"
pyproject = (ROOT / "pyproject.toml").read_text()
assert "python-pptx" in pyproject, \
"pyproject.toml does not declare python-pptx"
# python-pptx is in the [project.optional-dependencies] `slides` group.
# Locate the optional-dependencies table block, then check the `slides`
# array within it.
block_match = re.search(
r"\[project\.optional-dependencies\](.*?)(?=\n\[|\Z)",
pyproject, re.DOTALL)
assert block_match, \
"pyproject.toml has no [project.optional-dependencies] table"
block = block_match.group(1)
slides_match = re.search(r"slides\s*=\s*\[([^\]]*)\]", block, re.DOTALL)
assert slides_match, \
"pyproject.toml has no `slides` optional-dependency group"
assert "python-pptx" in slides_match.group(1), \
"python-pptx is not in the `slides` optional-dependency group"
def test_render_deck_removed():
"""REQ-257: render_deck.sh has been deleted (produced unthemed output)."""
old_script = ROOT / "scripts" / "render_deck.sh"
assert not old_script.exists(), \
"render_deck.sh still exists (should be deleted — produced unthemed output)"
# --- CI workflow (REQ-273) -------------------------------------------
def test_slides_ci_workflow_exists():
"""REQ-241: CI workflow for slides exists."""
assert SLIDES_WORKFLOW.is_file(), "slides.yml workflow not found"
@@ -118,6 +337,35 @@ def test_slides_ci_workflow_triggers_on_presentations():
"slides.yml does not invoke render_slides.sh"
def test_slides_ci_workflow_installs_python_pptx():
"""REQ-273: CI workflow installs python-pptx (via the `slides` extra)."""
text = SLIDES_WORKFLOW.read_text()
assert "python-pptx" in text or "[slides]" in text, \
"slides.yml does not install python-pptx / the slides extra"
assert "setup-python" in text, \
"slides.yml has no setup-python step"
def test_slides_ci_workflow_pins_cli_versions():
"""REQ-273: CI workflow pins marp-cli + mermaid-cli (no @latest)."""
text = SLIDES_WORKFLOW.read_text()
assert "marp-cli@4.5.0" in text, \
"slides.yml does not pin @marp-team/marp-cli@4.5.0"
assert "mermaid-cli@11.16.0" in text, \
"slides.yml does not pin @mermaid-js/mermaid-cli@11.16.0"
assert "@latest" not in text, \
"slides.yml still uses @latest (not pinned)"
def test_slides_ci_workflow_stages_python_pptx():
"""REQ-273: CI workflow `git add` list includes *-python.pptx."""
text = SLIDES_WORKFLOW.read_text()
assert "*-python.pptx" in text, \
"slides.yml git-add list does not stage *-python.pptx"
# --- Mermaid PNGs ----------------------------------------------------
def test_every_mmd_has_png():
"""REQ-240: every .mmd file has a corresponding .png."""
mmd_dir = ASSETS / "mmd"
@@ -134,6 +382,82 @@ def test_every_mmd_has_png():
assert not missing, f"PNGs missing for: {missing}"
def test_png_aspect_ratios_sane():
"""REQ-259/260: PNGs referenced in the marp deck have aspect ratios
in [0.4, 4.0] (suitable for 16:9 slides)."""
import struct
deck_text = MARP_DECK.read_text()
referenced = re.findall(r'!\[[^\]]*\]\(assets/png/([^)]+\.png)\)', deck_text)
assert referenced, "no PNGs referenced in the marp deck"
for png_name in referenced:
png_path = ASSETS / "png" / png_name
assert png_path.is_file(), f"referenced PNG not found: {png_name}"
with open(png_path, "rb") as fh:
data = fh.read(24)
assert data[:8] == b"\x89PNG\r\n\x1a\n", f"{png_name} is not a PNG"
w = struct.unpack(">I", data[16:20])[0]
h = struct.unpack(">I", data[20:24])[0]
ar = w / h
assert 0.4 <= ar <= 4.0, \
f"{png_name} aspect ratio {ar:.2f} outside [0.4, 4.0] ({w}x{h})"
# --- HTML: theme embed + image inlining + slide count ----------------
def test_html_embeds_theme():
"""REQ-262/274: the committed HTML embeds the S&P theme as literal
S&P colors (#D6002A — not just the --sp-red variable) + padding."""
html = HTML.read_text()
assert "#D6002A" in html, \
"committed HTML does not embed the literal S&P Red (#D6002A)"
assert "padding:" in html, "committed HTML does not embed padding rule"
def test_html_images_inlined_as_base64():
"""REQ-268/274: the rendered HTML is self-contained — zero
`src="assets/` references and at least one `data:image` per image
referenced in the -marp.md deck."""
html = HTML.read_text()
assert len(re.findall(r'src=["\']assets/', html)) == 0, \
"HTML still references external `assets/` images (not inlined)"
deck_text = MARP_DECK.read_text()
image_count = len(re.findall(r'!\[[^\]]*\]\(assets/', deck_text))
assert image_count > 0, "no images referenced in the marp deck"
data_uri_count = html.count("data:image")
assert data_uri_count >= image_count, \
f"HTML has {data_uri_count} data:image URIs but the deck " \
f"references {image_count} images (should be >=)"
def test_html_slide_count_matches_marp():
"""REQ-262: the committed HTML <section> count matches the marp deck
slide count (title + 20 main + 1 appendix = 22)."""
html = HTML.read_text()
section_count = html.count("<section ")
deck_text = MARP_DECK.read_text()
main_slides = len(re.findall(r"^## Slide ", deck_text, re.MULTILINE))
appendix_slides = len(re.findall(r"^## Appendix ", deck_text, re.MULTILINE))
expected = main_slides + appendix_slides + 1
assert section_count == expected, \
f"HTML has {section_count} sections, expected {expected} " \
f"({main_slides} main + {appendix_slides} appendix + 1 title)"
# --- python-pptx artifact (REQ-273/274) ------------------------------
def test_python_pptx_exists():
"""REQ-273/274: the python-pptx PPTX exists and is a valid OOXML zip
(the PPTX/zip signature `PK\x03\x04`)."""
assert PYTHON_PPTX.is_file(), \
f"python-pptx PPTX not found: {PYTHON_PPTX}"
with open(PYTHON_PPTX, "rb") as fh:
sig = fh.read(4)
assert sig == b"PK\x03\x04", \
f"python-pptx PPTX is not a valid zip (bad signature: {sig!r})"
# --- README (REQ-275) ------------------------------------------------
def test_readme_no_retired_decks():
"""REQ-243: presentations README does not list retired decks."""
readme = (PRESENTATIONS / "README.md").read_text()
@@ -153,221 +477,4 @@ def test_readme_no_old_deck_name():
def test_old_deck_files_removed():
"""REQ-245: the old nova-no-humans-platform* files are gone."""
old_files = sorted(PRESENTATIONS.glob("nova-no-humans-platform*"))
assert not old_files, f"old deck files still present: {old_files}"
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."""
text = MARP_DECK.read_text()
fm_match = re.match(r'^---\n(.*?)\n---', text, re.DOTALL)
assert fm_match, "Marp frontmatter not found"
frontmatter = fm_match.group(1)
# No v1.x version string in the footer or header lines
assert not re.search(r"v1\.\d+", frontmatter), \
f"Marp frontmatter still contains a version: {frontmatter}"
# No "Act" pagination artifact
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()
# The title slide is the first slide after the frontmatter
# Find the title block (between the frontmatter and the first --- separator)
after_fm = text.split("---\n", 2)[2] if text.startswith("---") else text
first_slide = after_fm.split("\n---\n")[0]
# The old subtitle was "v1.18 — Citizen Developer & Production-Grade Guidance"
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'"
# The old title should not appear in the audience-facing deck
# (speaker notes are not in the marp deck, so this is safe)
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 slides total.
v1.22 split slides 3 (Objectives+Anti-Goals) and 8 (Attestation
Matrix) to relieve overflow, increasing the count from 18 to 20."""
text = MARP_DECK.read_text()
# Count slide separators: each slide ends with --- (except the last)
# The frontmatter is one --- ... --- block, then each slide is separated by ---
# Count "## Slide" and "## Appendix" headings
slide_headings = re.findall(r"^## (?:Slide|Appendix) ", text, re.MULTILINE)
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)}: {slide_headings}"
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."""
text = MARP_DECK.read_text()
# Decision IDs like D-121, D-083
assert not re.search(r"\bD-\d{3}\b", text), \
"Marp deck contains D-### decision IDs"
# Requirement IDs like REQ-245
assert not re.search(r"\bREQ-\d{3}\b", text), \
"Marp deck contains REQ-### requirement IDs"
# Internal python file paths like outbox_writer.py, confidence_signal.py
# (allow .py only inside code blocks for the ROI formula? No — the deck
# should not cite internal file paths at all)
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", text), \
"Marp deck contains internal .py file paths"
def test_source_md_no_internal_citations_in_slides():
"""REQ-252: the source-of-truth markdown keeps internal citations only
in speaker notes, not in the audience-facing slide body. Speaker notes
are blockquoted (> ) — we check non-blockquote lines for D-###/REQ-###."""
text = SOURCE_MD.read_text()
# Split into lines; exclude blockquote lines (speaker notes) and the
# header frontmatter (> ... at the top)
in_note = False
body_lines = []
for line in text.splitlines():
if line.lstrip().startswith(">"):
in_note = True
continue
if in_note and line.strip() == "":
in_note = False
continue
if not in_note:
body_lines.append(line)
body = "\n".join(body_lines)
# Decision IDs and REQ IDs should not appear in the slide body
assert not re.search(r"\bD-\d{3}\b", body), \
"Source markdown slide body contains D-### decision IDs"
assert not re.search(r"\bREQ-\d{3}\b", body), \
"Source markdown slide body contains REQ-### requirement IDs"
def test_source_md_no_badges():
"""REQ-252: no maturity badges in the source-of-truth markdown."""
text = SOURCE_MD.read_text()
assert "badge" not in text.lower(), \
"Source markdown still contains badge spans"
# --- v1.22 layout/aspect-ratio/theme-structural tests (REQ-262) ---
def test_theme_css_has_section_padding():
"""REQ-254: theme CSS has a section padding rule (root cause fix)."""
css = THEME_CSS.read_text()
assert "padding:" in css, "theme CSS has no padding rule"
# The section rule must have padding (not just table/td padding)
assert re.search(r"section\s*\{[^}]*padding:", css, re.DOTALL), \
"theme CSS has no padding on the section rule"
def test_theme_css_suppresses_title_chrome():
"""REQ-256: title slides suppress header/footer chrome."""
css = THEME_CSS.read_text()
assert "section.title header" in css, \
"theme CSS does not suppress title-slide header"
assert "section.title footer" in css, \
"theme CSS does not suppress title-slide footer"
assert "display: none" in css, \
"theme CSS does not set display:none on title chrome"
def test_theme_css_has_aspect_ratio_aware_images():
"""REQ-255: image rules use object-fit + max-width (not blunt max-height only)."""
css = THEME_CSS.read_text()
assert "object-fit" in css, \
"theme CSS does not use object-fit for images"
assert "max-width" in css, \
"theme CSS does not set max-width for images"
def test_png_aspect_ratios_sane():
"""REQ-259/260: PNGs referenced in the marp deck have aspect ratios
in [0.4, 4.0] (suitable for 16:9 slides with img.tall/img.wide classes).
Only checks PNGs actually referenced in the current marp deck —
legacy/unused PNGs are not checked (GRILL revision 1)."""
import struct
deck_text = MARP_DECK.read_text()
# Extract all referenced PNG paths: ![...](assets/png/X.png)
referenced = re.findall(r'!\[[^\]]*\]\(assets/png/([^)]+\.png)\)', deck_text)
assert referenced, "no PNGs referenced in the marp deck"
for png_name in referenced:
png_path = ASSETS / "png" / png_name
assert png_path.is_file(), f"referenced PNG not found: {png_name}"
with open(png_path, "rb") as fh:
data = fh.read(24)
assert data[:8] == b"\x89PNG\r\n\x1a\n", f"{png_name} is not a PNG"
w = struct.unpack(">I", data[16:20])[0]
h = struct.unpack(">I", data[20:24])[0]
ar = w / h
assert 0.4 <= ar <= 4.0, \
f"{png_name} aspect ratio {ar:.2f} outside [0.4, 4.0] ({w}x{h})"
def test_render_slides_has_2x_scale():
"""REQ-258: render_slides.sh uses -s 2 (2x scale) and -b transparent."""
text = RENDER_SCRIPT.read_text()
assert "-s 2" in text, "render_slides.sh does not use -s 2 (2x scale)"
assert "-b transparent" in text, \
"render_slides.sh does not use -b transparent"
def test_render_slides_pins_cli_versions():
"""REQ-257: render_slides.sh pins marp-cli and mermaid-cli versions
(no @latest)."""
text = RENDER_SCRIPT.read_text()
assert "marp-cli@" in text, "render_slides.sh does not pin marp-cli"
assert "mermaid-cli@" in text, \
"render_slides.sh does not pin mermaid-cli"
assert "@latest" not in text, \
"render_slides.sh still uses @latest (not pinned)"
def test_render_deck_removed():
"""REQ-257: render_deck.sh has been deleted (produced unthemed output)."""
old_script = ROOT / "scripts" / "render_deck.sh"
assert not old_script.exists(), \
"render_deck.sh still exists (should be deleted — produced unthemed output)"
def test_html_embeds_theme():
"""REQ-262: the committed HTML embeds the S&P theme (--sp-red + padding
in the inline <style> block)."""
html = (PRESENTATIONS / "nova-autonomous-cloud-delivery.html").read_text()
assert "--sp-red" in html, "committed HTML does not embed --sp-red"
assert "padding:" in html, "committed HTML does not embed padding rule"
def test_html_slide_count_matches_marp():
"""REQ-262: the committed HTML <section> count matches the marp deck
slide count (title + 20 main + 1 appendix = 22)."""
html = (PRESENTATIONS / "nova-autonomous-cloud-delivery.html").read_text()
section_count = html.count("<section ")
deck_text = MARP_DECK.read_text()
main_slides = len(re.findall(r"^## Slide ", deck_text, re.MULTILINE))
appendix_slides = len(re.findall(r"^## Appendix ", deck_text, re.MULTILINE))
# +1 for the title slide (which is an H1, not "## Slide")
expected = main_slides + appendix_slides + 1
assert section_count == expected, \
f"HTML has {section_count} sections, expected {expected} " \
f"({main_slides} main + {appendix_slides} appendix + 1 title)"
assert not old_files, f"old deck files still present: {old_files}"