301aa2c8d8
Nova Slides Render / render (push) Failing after 58s
Marp deck (nova-autonomous-cloud-delivery-marp.md): synthesize from updated
source-of-truth; 18 main + 1 appendix slides; frontmatter — title 'Nova —
The Autonomous Cloud Delivery Platform', footer without version + without
'Act %{page}/5', title-slide subtitle 'Product Development & Citizen
Developer Overview'; no badges; embedded PNGs.
Talking points (nova-autonomous-cloud-delivery-talking-points.md):
re-distilled to 18-slide + A1 structure.
README.md: update deck title, audience, slide count (18 main + 1 appendix),
directory layout, remove badge docs, update deck table + render commands +
filenames. Document the v1.21 rename + restructure.
Theme CSS (nova-sp-theme.css): fix Appendix A1 table readability — tables
now have explicit white body + black text on any slide background
(including dark/title slides). Item 32.
Tests (test_slides_pipeline.py): add v1.21 assertions — no badges; no
version in footer/title slide; 18 main + 1 appendix slides; no D-###/REQ-
###/.py paths in audience-facing Marp deck or source slide body; old deck
files removed; render script default renamed; README references new deck
name. Update deck path in test_regression_cap023_024.py +
core/regression_verify.py CAP-024 (filename + 18-19 slide range, drop 'Arc
Preview' check per item 3).
attach_release_asset.py: usage example filename updated.
---ci---
project: acdl
phase: 3
milestone: v1.21
status: execute
phase_role: execution
---/ci---
268 lines
11 KiB
Python
268 lines
11 KiB
Python
"""REQ-239..243 (v1.20) + REQ-245,251,252 (v1.21): 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):
|
|
- Deck renamed to nova-autonomous-cloud-delivery*
|
|
- No maturity badges in the Marp deck
|
|
- No version in the Marp footer/title slide
|
|
- 18 main + 1 appendix slides
|
|
- No D-###/REQ-###/internal .py paths in audience-facing slides
|
|
- Title is "Nova — The Autonomous Cloud Delivery Platform"
|
|
"""
|
|
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"
|
|
RENDER_SCRIPT = ROOT / "scripts" / "render_slides.sh"
|
|
SLIDES_WORKFLOW = ROOT / ".github" / "workflows" / "slides.yml"
|
|
|
|
|
|
def test_sp_theme_css_exists():
|
|
"""REQ-239: nova-sp-theme.css exists."""
|
|
assert THEME_CSS.is_file(), f"theme CSS not found: {THEME_CSS}"
|
|
|
|
|
|
def test_sp_theme_css_has_snp_colors():
|
|
"""REQ-239: CSS contains 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."""
|
|
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."""
|
|
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"
|
|
|
|
|
|
def test_marp_deck_not_using_default_theme():
|
|
"""The Marp deck must not use 'theme: default'."""
|
|
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"
|
|
|
|
|
|
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"
|
|
import os
|
|
assert os.access(RENDER_SCRIPT, os.X_OK), "render_slides.sh not executable"
|
|
|
|
|
|
def test_render_slides_script_renders_mermaid():
|
|
"""REQ-240: render_slides.sh renders mermaid diagrams."""
|
|
text = RENDER_SCRIPT.read_text()
|
|
assert "mermaid-cli" in text or "mmdc" in text, \
|
|
"render_slides.sh does not invoke mermaid-cli"
|
|
assert "sp-theme.json" in text, \
|
|
"render_slides.sh does not reference sp-theme.json"
|
|
|
|
|
|
def test_render_slides_script_renders_marp():
|
|
"""REQ-240: render_slides.sh renders Marp HTML + PPTX."""
|
|
text = RENDER_SCRIPT.read_text()
|
|
assert "marp-cli" in text, "render_slides.sh does not invoke marp-cli"
|
|
assert ".html" in text, "render_slides.sh does not produce HTML"
|
|
assert ".pptx" in text, "render_slides.sh does not produce PPTX"
|
|
|
|
|
|
def test_render_slides_default_deck_renamed():
|
|
"""REQ-245: render_slides.sh default deck is nova-autonomous-cloud-delivery."""
|
|
text = RENDER_SCRIPT.read_text()
|
|
assert "nova-autonomous-cloud-delivery" in text, \
|
|
"render_slides.sh does not default to nova-autonomous-cloud-delivery"
|
|
|
|
|
|
def test_slides_ci_workflow_exists():
|
|
"""REQ-241: CI workflow for slides exists."""
|
|
assert SLIDES_WORKFLOW.is_file(), "slides.yml workflow not found"
|
|
|
|
|
|
def test_slides_ci_workflow_triggers_on_presentations():
|
|
"""REQ-241: CI workflow triggers on docs/presentations/ changes."""
|
|
text = SLIDES_WORKFLOW.read_text()
|
|
assert "docs/presentations" in text, \
|
|
"slides.yml does not trigger on docs/presentations/"
|
|
assert "render_slides.sh" in text, \
|
|
"slides.yml does not invoke render_slides.sh"
|
|
|
|
|
|
def test_every_mmd_has_png():
|
|
"""REQ-240: every .mmd file has a corresponding .png."""
|
|
mmd_dir = ASSETS / "mmd"
|
|
png_dir = ASSETS / "png"
|
|
if not mmd_dir.is_dir():
|
|
pytest.skip("no .mmd directory")
|
|
mmd_files = sorted(mmd_dir.glob("*.mmd"))
|
|
assert len(mmd_files) > 0, "no .mmd files found"
|
|
missing = []
|
|
for mmd in mmd_files:
|
|
png = png_dir / f"{mmd.stem}.png"
|
|
if not png.is_file():
|
|
missing.append(mmd.name)
|
|
assert not missing, f"PNGs missing for: {missing}"
|
|
|
|
|
|
def test_readme_no_retired_decks():
|
|
"""REQ-243: presentations README does not list retired decks."""
|
|
readme = (PRESENTATIONS / "README.md").read_text()
|
|
assert "how-the-platform-works" not in readme, \
|
|
"README still references retired 'how-the-platform-works' deck"
|
|
assert "the-developer-experience" not in readme, \
|
|
"README still references retired 'the-developer-experience' deck"
|
|
|
|
|
|
def test_readme_no_old_deck_name():
|
|
"""REQ-245: README references the new deck name, not the old one."""
|
|
readme = (PRESENTATIONS / "README.md").read_text()
|
|
assert "nova-autonomous-cloud-delivery" in readme, \
|
|
"README does not reference nova-autonomous-cloud-delivery"
|
|
|
|
|
|
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: 18 main slides + 1 appendix = 19 slides total."""
|
|
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) == 18, \
|
|
f"expected 18 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" |