Six-skill presentation system: ingest talks into a rhetoric vault, run interactive clarification, generate a speaker profile, create presentations that match your documented patterns, produce the deck illustrations + thumbnail visual layer, and publish talk pages to a Jekyll shownotes site. Includes a 102-entry Presentation Patterns taxonomy (91 observable, 11 unobservable go-live items) for scoring, brainstorming, and go-live preparation.
86
92%
Does it follow best practices?
Impact
86%
1.24xAverage score across 26 eval scenarios
Advisory
Suggest reviewing before use
"""Tests for outline_schema.py — pydantic validators on the outline source-of-truth."""
import copy
from pathlib import Path
import pytest
import yaml
from pydantic import ValidationError
FIXTURE = Path(__file__).parent / "fixtures" / "outline-example.yaml"
@pytest.fixture(scope="session")
def base_data():
"""Parsed YAML of the canonical valid fixture — returned as a dict.
Tests use copy.deepcopy(base_data) and mutate one field to assert a
specific validator fires. Provides fixed test data per testing-standards;
each test owns its mutation."""
return yaml.safe_load(FIXTURE.read_text(encoding="utf-8"))
# ── Happy path ────────────────────────────────────────────────────────
def test_fixture_loads_clean(outline_schema):
outline = outline_schema.load_outline(FIXTURE)
assert outline.talk.title == "Demo Talk"
assert len(outline.chapters) == 3
assert len(outline.slides) == 8
def test_pattern_enum_discovered(outline_schema):
# 78 patterns + 25 antipatterns per _index.md taxonomy
assert len(outline_schema.PATTERN_IDS) == 78
assert len(outline_schema.ANTIPATTERN_IDS) == 25
assert "sparkline" in outline_schema.PATTERN_IDS
assert "slideuments" in outline_schema.ANTIPATTERN_IDS
# ── Talk-level validators ────────────────────────────────────────────
def test_rejects_unknown_architecture(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["architecture"] = "freeform-vibes"
with pytest.raises(ValidationError, match="architecture"):
outline_schema.Outline.model_validate(data)
def test_rejects_empty_speakers(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["speakers"] = []
with pytest.raises(ValidationError):
outline_schema.Outline.model_validate(data)
# ── Slide-level validators ───────────────────────────────────────────
def test_rejects_unknown_chapter_ref(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["slides"][0]["chapter"] = "ch99"
with pytest.raises(ValidationError, match="unknown chapter"):
outline_schema.Outline.model_validate(data)
def test_rejects_duplicate_slide_numbers(outline_schema, base_data):
data = copy.deepcopy(base_data)
# Add a duplicate slide entry without breaking slide_refs (which now validate too)
last = copy.deepcopy(data["slides"][-1])
last["n"] = data["slides"][0]["n"]
data["slides"].append(last)
data["talk"]["slide_budget"] = 99
with pytest.raises(ValidationError, match="duplicate slide numbers"):
outline_schema.Outline.model_validate(data)
def test_rejects_out_of_order_slides(outline_schema, base_data):
data = copy.deepcopy(base_data)
# Swap first two slide numbers without duplicating
data["slides"][0]["n"], data["slides"][1]["n"] = (
data["slides"][1]["n"], data["slides"][0]["n"],
)
with pytest.raises(ValidationError, match="ascending order"):
outline_schema.Outline.model_validate(data)
def test_rejects_exception_without_justification(outline_schema, base_data):
data = copy.deepcopy(base_data)
slide = next(s for s in data["slides"] if s["format"] == "EXCEPTION")
slide["format_justification"] = ""
with pytest.raises(ValidationError, match="format_justification"):
outline_schema.Outline.model_validate(data)
# ── big_idea singleton ───────────────────────────────────────────────
def test_rejects_zero_big_ideas(outline_schema, base_data):
data = copy.deepcopy(base_data)
for s in data["slides"]:
s["big_idea"] = False
with pytest.raises(ValidationError, match="big_idea"):
outline_schema.Outline.model_validate(data)
def test_rejects_multiple_big_ideas(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["slides"][0]["big_idea"] = True
data["slides"][1]["big_idea"] = True
with pytest.raises(ValidationError, match="big_idea"):
outline_schema.Outline.model_validate(data)
# ── Speaker attribution ──────────────────────────────────────────────
def test_rejects_speaker_on_single_speaker_line(outline_schema, base_data):
data = copy.deepcopy(base_data)
# Find first line item and attribute it (illegal in single-speaker mode)
for slide in data["slides"]:
for item in slide.get("script", []):
if item.get("line") is not None:
item["speaker"] = "Speaker One"
break
else:
continue
break
with pytest.raises(ValidationError, match="single-speaker talk must not"):
outline_schema.Outline.model_validate(data)
def test_requires_speaker_on_multi_speaker_line(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["speakers"] = ["Speaker One", "Speaker Two"]
with pytest.raises(ValidationError, match="multi-speaker talk requires"):
outline_schema.Outline.model_validate(data)
def test_rejects_unknown_speaker_attribution(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["speakers"] = ["Speaker One", "Speaker Two"]
# Now attribute every line, but use a name not in talk.speakers
for slide in data["slides"]:
for item in slide.get("script", []):
if item.get("line") is not None:
item["speaker"] = "Speaker Three"
with pytest.raises(ValidationError, match="not in talk.speakers"):
outline_schema.Outline.model_validate(data)
def test_accepts_valid_multi_speaker(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["speakers"] = ["Speaker One", "Speaker Two"]
for slide in data["slides"]:
for item in slide.get("script", []):
if item.get("line") is not None:
item["speaker"] = "Speaker One"
outline = outline_schema.Outline.model_validate(data)
assert len(outline.talk.speakers) == 2
# ── Slide budget ─────────────────────────────────────────────────────
def test_rejects_slide_budget_exceeded(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["slide_budget"] = 5
with pytest.raises(ValidationError, match="slide budget exceeded"):
outline_schema.Outline.model_validate(data)
def test_builds_count_toward_budget(outline_schema, base_data):
data = copy.deepcopy(base_data)
# Fixture has 8 slide entries; slide n=2 has 4 build steps.
# Expanded count = 7 (non-build slides) + 4 (builds on slide 2) = 11
data["talk"]["slide_budget"] = 11
outline_schema.Outline.model_validate(data) # exactly at budget — OK
data["talk"]["slide_budget"] = 10
with pytest.raises(ValidationError, match="slide budget exceeded"):
outline_schema.Outline.model_validate(data)
# ── Pattern validation ───────────────────────────────────────────────
def test_rejects_antipattern_in_outline(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["applied_patterns"].append({"id": "slideuments"})
with pytest.raises(ValidationError, match="antipattern"):
outline_schema.Outline.model_validate(data)
def test_rejects_unknown_pattern_id(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["applied_patterns"].append({"id": "fictional-pattern"})
with pytest.raises(ValidationError, match="not found"):
outline_schema.Outline.model_validate(data)
def test_rejects_instance_field_on_wrong_pattern(outline_schema, base_data):
data = copy.deepcopy(base_data)
# flavors only belongs on opening-punch — putting it on bookends should fail
data["talk"]["applied_patterns"].append({
"id": "bookends",
"flavors": ["personal"],
})
with pytest.raises(ValidationError, match="does not accept"):
outline_schema.Outline.model_validate(data)
def test_accepts_deliver_phase_pattern(outline_schema, base_data):
# `mentor` lives in deliver/ but shapes the outline — must be accepted
data = copy.deepcopy(base_data)
data["talk"]["applied_patterns"].append({"id": "mentor"})
outline = outline_schema.Outline.model_validate(data)
assert any(p.id == "mentor" for p in outline.talk.applied_patterns)
# ── Callback ledger ──────────────────────────────────────────────────
def test_rejects_unpaid_callback_plant(outline_schema, base_data):
data = copy.deepcopy(base_data)
# Remove the pay for receipt-motif (currently on slide n=7)
for slide in data["slides"]:
slide["callbacks"] = [
cb for cb in slide.get("callbacks", [])
if not (cb.get("kind") == "pay" and cb.get("id") == "receipt-motif")
]
with pytest.raises(ValidationError, match="unpaid plants"):
outline_schema.Outline.model_validate(data)
def test_rejects_orphan_callback_pay(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["slides"][0]["callbacks"].append(
{"kind": "pay", "id": "no-such-plant"},
)
with pytest.raises(ValidationError, match="pays without plants"):
outline_schema.Outline.model_validate(data)
# ── Script item shape ────────────────────────────────────────────────
def test_rejects_script_item_with_multiple_content_fields(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["slides"][0]["script"][0] = {"cue": "SLIDE 1 UP", "line": "Hello."}
with pytest.raises(ValidationError, match="exactly one"):
outline_schema.Outline.model_validate(data)
def test_rejects_script_item_with_no_content(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["slides"][0]["script"][0] = {}
with pytest.raises(ValidationError, match="exactly one"):
outline_schema.Outline.model_validate(data)
# ── Slide-0 title cards + DEMO interludes ────────────────────────────
def test_accepts_slide_n_zero(outline_schema, base_data):
"""Title-card / pre-talk slides start at n=0 in some talk shapes."""
data = copy.deepcopy(base_data)
# Shift all slide numbers down by 1 so the first slide is n=0;
# rewrite argument_beats slide_refs to match the new numbering.
for s in data["slides"]:
s["n"] -= 1
for c in data["chapters"]:
for beat in c.get("argument_beats", []):
beat["slide_refs"] = [r - 1 for r in beat.get("slide_refs", [])]
data["talk"]["slide_budget"] = 30
outline = outline_schema.Outline.model_validate(data)
assert outline.slides[0].n == 0
def test_accepts_demo_format(outline_schema, base_data):
"""DEMO is a production-interlude slide — live terminal, no image prompt."""
data = copy.deepcopy(base_data)
data["slides"].append({
"n": 99,
"chapter": "ch1",
"title": "DEMO 01 — Live Terminal",
"format": "DEMO",
"visual": "Live terminal. Claude Code TUI.",
"script": [
{"cue": "TERMINAL UP"},
{"line": "Let me show you something."},
],
})
data["talk"]["slide_budget"] = 30
outline = outline_schema.Outline.model_validate(data)
assert outline.slides[-1].format.value == "DEMO"
def test_accepts_title_format(outline_schema, base_data):
"""TITLE is a title-card slide — passive display, often n=0."""
data = copy.deepcopy(base_data)
data["slides"].append({
"n": 99,
"chapter": "ch1",
"title": "Title Card",
"format": "TITLE",
})
data["talk"]["slide_budget"] = 30
outline = outline_schema.Outline.model_validate(data)
assert outline.slides[-1].format.value == "TITLE"
def test_accepts_cuttable_flag(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["slides"][-1]["cuttable"] = True
data["chapters"][-1]["cuttable"] = True
outline = outline_schema.Outline.model_validate(data)
assert outline.slides[-1].cuttable is True
assert outline.chapters[-1].cuttable is True
def test_accepts_chapter_accent(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["chapters"][0]["accent"] = "red"
outline = outline_schema.Outline.model_validate(data)
assert outline.chapters[0].accent == "red"
# ── Speaker-attributed parentheticals (screenplay form) ──────────────
def test_accepts_speaker_on_parenthetical_multi_speaker(outline_schema, base_data):
"""In screenplay form, a parenthetical can belong to a specific speaker
(e.g., PATRICK (to Baruch))."""
data = copy.deepcopy(base_data)
data["talk"]["speakers"] = ["Speaker One", "Speaker Two"]
# Attribute all line items so multi-speaker validation passes
for slide in data["slides"]:
for item in slide.get("script", []):
if item.get("line") is not None:
item["speaker"] = "Speaker One"
# Add a speaker-attributed parenthetical
data["slides"][0]["script"].insert(0, {
"speaker": "Speaker Two",
"parenthetical": "(to Speaker One)",
})
outline = outline_schema.Outline.model_validate(data)
assert outline.slides[0].script[0].speaker == "Speaker Two"
def test_rejects_speaker_on_parenthetical_single_speaker(outline_schema, base_data):
"""Single-speaker talks must not attribute parentheticals — there's only one voice."""
data = copy.deepcopy(base_data)
data["slides"][0]["script"].insert(0, {
"speaker": "Speaker One",
"parenthetical": "(beat)",
})
with pytest.raises(ValidationError, match="must not attribute parentheticals"):
outline_schema.Outline.model_validate(data)
def test_rejects_speaker_on_cue(outline_schema, base_data):
"""Cues are scene-level production directions, never attributed."""
data = copy.deepcopy(base_data)
data["slides"][0]["script"][0] = {
"speaker": "Speaker One",
"cue": "SLIDE UP",
}
with pytest.raises(ValidationError, match="does not apply to cue"):
outline_schema.Outline.model_validate(data)
def test_rejects_unknown_speaker_on_parenthetical(outline_schema, base_data):
"""Attributed parentheticals must use a real speaker name."""
data = copy.deepcopy(base_data)
data["talk"]["speakers"] = ["Speaker One", "Speaker Two"]
for slide in data["slides"]:
for item in slide.get("script", []):
if item.get("line") is not None:
item["speaker"] = "Speaker One"
data["slides"][0]["script"].insert(0, {
"speaker": "Speaker Three",
"parenthetical": "(to Speaker One)",
})
with pytest.raises(ValidationError, match="not in talk.speakers"):
outline_schema.Outline.model_validate(data)
# ── Interludes ───────────────────────────────────────────────────────
def test_accepts_interlude(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["interludes"] = [{
"id": "demo-01",
"after_slide": 1,
"chapter": "ch1",
"title": "DEMO 01 — Cold Open",
"script": [
{"cue": "TERMINAL UP"},
{"line": "Let me show you something."},
],
}]
outline = outline_schema.Outline.model_validate(data)
assert len(outline.interludes) == 1
assert outline.interludes[0].after_slide == 1
def test_rejects_interlude_unknown_chapter(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["interludes"] = [{
"id": "demo-01",
"after_slide": 1,
"chapter": "ch99",
"title": "DEMO 01",
"script": [{"cue": "TERMINAL UP"}],
}]
with pytest.raises(ValidationError, match="unknown chapter"):
outline_schema.Outline.model_validate(data)
def test_rejects_interlude_bad_anchor(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["interludes"] = [{
"id": "demo-01",
"after_slide": 999,
"chapter": "ch1",
"title": "DEMO 01",
"script": [{"cue": "TERMINAL UP"}],
}]
with pytest.raises(ValidationError, match="after_slide=999"):
outline_schema.Outline.model_validate(data)
def test_rejects_duplicate_interlude_ids(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["interludes"] = [
{
"id": "demo-01",
"after_slide": 1,
"chapter": "ch1",
"title": "DEMO 01",
"script": [{"cue": "TERMINAL UP"}],
},
{
"id": "demo-01",
"after_slide": 2,
"chapter": "ch1",
"title": "DEMO 01 again",
"script": [{"cue": "TERMINAL UP"}],
},
]
with pytest.raises(ValidationError, match="duplicate interlude ids"):
outline_schema.Outline.model_validate(data)
def test_interlude_callbacks_count_toward_ledger(outline_schema, base_data):
"""An interlude that plants a callback must be paid (somewhere)."""
data = copy.deepcopy(base_data)
data["interludes"] = [{
"id": "demo-01",
"after_slide": 1,
"chapter": "ch1",
"title": "DEMO 01",
"script": [{"cue": "TERMINAL UP"}],
"callbacks": [{"kind": "plant", "id": "demo-plant"}],
}]
with pytest.raises(ValidationError, match="unpaid plants"):
outline_schema.Outline.model_validate(data)
# ── Slug + collapsed-spec metadata ───────────────────────────────────
def test_requires_slug(outline_schema, base_data):
"""slug is mandatory — it names the presenterm/pptx deck and the
shownotes URL path."""
data = copy.deepcopy(base_data)
del data["talk"]["slug"]
with pytest.raises(ValidationError):
outline_schema.Outline.model_validate(data)
def test_rejects_non_kebab_slug(outline_schema, base_data):
data = copy.deepcopy(base_data)
bad_slugs = [
"Demo Talk", # spaces + caps
"demo_talk", # underscore
"demo--talk", # double dash
"-demo-talk", # leading dash
"demo-talk-", # trailing dash
"DemoTalk", # camelCase
]
for slug in bad_slugs:
data["talk"]["slug"] = slug
with pytest.raises(ValidationError, match="kebab-case"):
outline_schema.Outline.model_validate(data)
def test_accepts_kebab_slugs(outline_schema, base_data):
data = copy.deepcopy(base_data)
good_slugs = [
"demo",
"demo-talk",
"geecon-2026-absolutely-right",
"devoxx-uk-2026-300-tokens",
"talk-1",
"v3",
]
for slug in good_slugs:
data["talk"]["slug"] = slug
outline = outline_schema.Outline.model_validate(data)
assert outline.talk.slug == slug
def test_accepts_spec_fields(outline_schema, base_data):
"""Optional spec metadata collapsed from the retired presentation-spec.md."""
data = copy.deepcopy(base_data)
data["talk"]["thesis"] = "Treat context as a first-class artifact."
data["talk"]["shownotes_url_base"] = "https://speaking.example.com/"
data["talk"]["commercial_intent"] = "subtle"
data["talk"]["profanity_register"] = "moderate — verbal only, never on slides"
data["talk"]["must_include"] = ["the receipt motif", "Mr. Fusion callback"]
data["talk"]["must_avoid"] = ["no competitor disparagement"]
data["talk"]["catalog_reference"] = "sessions-catalog.md#absolutely-right"
data["talk"]["delivery_count"] = 1
data["talk"]["delivery_date"] = "2026-05-14"
outline = outline_schema.Outline.model_validate(data)
assert outline.talk.thesis.startswith("Treat context")
assert outline.talk.delivery_count == 1
assert outline.talk.delivery_date == "2026-05-14"
def test_rejects_bad_delivery_date(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["delivery_date"] = "May 14, 2026"
with pytest.raises(ValidationError, match="ISO YYYY-MM-DD"):
outline_schema.Outline.model_validate(data)
def test_rejects_zero_delivery_count(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["talk"]["delivery_count"] = 0
with pytest.raises(ValidationError):
outline_schema.Outline.model_validate(data)
# ── Strict-schema + structural-integrity validators (PR-#45 review) ──
def test_rejects_unknown_top_level_field(outline_schema, base_data):
"""extra='forbid' on every model — misspelled YAML keys fail loud."""
data = copy.deepcopy(base_data)
data["talk"]["audiance"] = "typo!" # 'audience' misspelled
with pytest.raises(ValidationError, match="audiance"):
outline_schema.Outline.model_validate(data)
def test_rejects_unknown_slide_field(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["slides"][0]["formaat"] = "FULL"
with pytest.raises(ValidationError, match="formaat"):
outline_schema.Outline.model_validate(data)
def test_rejects_duplicate_chapter_ids(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["chapters"][1]["id"] = data["chapters"][0]["id"]
with pytest.raises(ValidationError, match="duplicate chapter ids"):
outline_schema.Outline.model_validate(data)
def test_rejects_duplicate_build_steps(outline_schema, base_data):
data = copy.deepcopy(base_data)
slide_with_builds = next(s for s in data["slides"] if s.get("builds"))
slide_with_builds["builds"][1]["step"] = slide_with_builds["builds"][0]["step"]
with pytest.raises(ValidationError, match="contiguous starting"):
outline_schema.Outline.model_validate(data)
def test_rejects_out_of_order_build_steps(outline_schema, base_data):
data = copy.deepcopy(base_data)
slide_with_builds = next(s for s in data["slides"] if s.get("builds"))
slide_with_builds["builds"][0]["step"], slide_with_builds["builds"][-1]["step"] = (
slide_with_builds["builds"][-1]["step"],
slide_with_builds["builds"][0]["step"],
)
with pytest.raises(ValidationError, match="ascending"):
outline_schema.Outline.model_validate(data)
def test_rejects_builds_not_starting_at_zero(outline_schema, base_data):
"""Build sequences must start at step 0 — the empty frame."""
data = copy.deepcopy(base_data)
slide_with_builds = next(s for s in data["slides"] if s.get("builds"))
for i, b in enumerate(slide_with_builds["builds"]):
b["step"] = i + 1 # shift to start at 1
with pytest.raises(ValidationError, match="contiguous starting at 0"):
outline_schema.Outline.model_validate(data)
def test_rejects_builds_with_holes(outline_schema, base_data):
"""Build sequences must be contiguous — no gaps."""
data = copy.deepcopy(base_data)
slide_with_builds = next(s for s in data["slides"] if s.get("builds"))
slide_with_builds["builds"][-1]["step"] = 99 # gap
with pytest.raises(ValidationError, match="contiguous"):
outline_schema.Outline.model_validate(data)
def test_requires_format_on_every_slide(outline_schema, base_data):
data = copy.deepcopy(base_data)
del data["slides"][0]["format"]
with pytest.raises(ValidationError, match="format"):
outline_schema.Outline.model_validate(data)
def test_rejects_argument_beat_slide_ref_to_missing_slide(outline_schema, base_data):
data = copy.deepcopy(base_data)
data["chapters"][0]["argument_beats"][0]["slide_refs"] = [999]
with pytest.raises(ValidationError, match="slide_ref 999"):
outline_schema.Outline.model_validate(data)
# ── Partial (narrative-phase) loader ─────────────────────────────────
def test_partial_accepts_talk_only(outline_schema, base_data):
"""Phase 1: only the talk block exists — no chapters, no slides."""
data = {"talk": copy.deepcopy(base_data["talk"])}
partial = outline_schema.PartialOutline.model_validate(data)
assert partial.talk.title == "Demo Talk"
assert partial.chapters == []
assert partial.slides == []
def test_partial_accepts_chapters_without_slides(outline_schema, base_data):
"""Phase 2: chapters authored, slides not yet. Beats carry no slide_refs —
slides do not exist, so there is nothing to reference."""
chapters = copy.deepcopy(base_data["chapters"])
for c in chapters:
for beat in c.get("argument_beats", []):
beat["slide_refs"] = []
data = {"talk": copy.deepcopy(base_data["talk"]), "chapters": chapters}
partial = outline_schema.PartialOutline.model_validate(data)
assert len(partial.chapters) == 3
assert partial.slides == []
def test_partial_rejects_slide_refs_without_slides(outline_schema, base_data):
"""Phase 2 invariant: a beat may not reference a slide that does not exist.
With no slides authored, every slide_ref is invalid."""
chapters = copy.deepcopy(base_data["chapters"])
chapters[0]["argument_beats"][0]["slide_refs"] = [1]
data = {"talk": copy.deepcopy(base_data["talk"]), "chapters": chapters}
with pytest.raises(ValidationError, match="slide_ref 1"):
outline_schema.PartialOutline.model_validate(data)
def test_partial_still_validates_talk_fields(outline_schema, base_data):
"""Field-level validators on talk still fire in partial mode."""
data = {"talk": copy.deepcopy(base_data["talk"])}
data["talk"]["architecture"] = "freeform-vibes"
with pytest.raises(ValidationError, match="architecture"):
outline_schema.PartialOutline.model_validate(data)
def test_partial_rejects_unknown_field(outline_schema, base_data):
"""extra='forbid' holds — misspelled keys fail loud even in partial mode."""
data = {"talk": copy.deepcopy(base_data["talk"]), "chapterz": []}
with pytest.raises(ValidationError, match="chapterz"):
outline_schema.PartialOutline.model_validate(data)
def test_partial_loader_accepts_full_outline(outline_schema):
"""--partial works at any phase: a complete outline also loads partially."""
partial = outline_schema.load_outline_partial(FIXTURE)
assert len(partial.chapters) == 3
assert len(partial.slides) == 8
# ── CLI: --emit-json contract ─────────────────────────────────────────
def test_cli_emit_json_dumps_validated_outline(outline_schema, capsys):
"""`--emit-json <path>` writes the validated outline to stdout as JSON.
Shownotes-publisher and other downstream skills rely on this contract
to pull structured fields (talk.title, talk.slug, etc.) without
re-parsing YAML by hand."""
import json as _json
rc = outline_schema.main([
"outline_schema.py",
"--emit-json",
str(FIXTURE),
])
assert rc == 0
captured = capsys.readouterr()
assert captured.err == ""
payload = _json.loads(captured.out)
assert payload["talk"]["title"] == "Demo Talk"
assert payload["talk"]["slug"] == "demo-conf-2026-demo-talk"
assert isinstance(payload["chapters"], list)
assert isinstance(payload["slides"], list)
def test_cli_default_mode_prints_ok_summary(outline_schema, capsys):
"""Default mode (no flag) still prints the human-readable OK line."""
rc = outline_schema.main(["outline_schema.py", str(FIXTURE)])
assert rc == 0
captured = capsys.readouterr()
assert captured.out.startswith("OK: ")
assert "slides across" in captured.out
def test_cli_emit_json_validation_failure_to_stderr(outline_schema, capsys, tmp_path):
"""Validation failures still go to stderr with exit 1 — JSON mode does
not silently emit a partial payload on failure."""
bad = tmp_path / "bad-outline.yaml"
bad.write_text("talk: {}\n", encoding="utf-8")
rc = outline_schema.main([
"outline_schema.py",
"--emit-json",
str(bad),
])
assert rc == 1
captured = capsys.readouterr()
assert captured.out == ""
assert "FAIL:" in captured.err.github
eval-resources
humor-postmortem-blind-spots
qr-bitly-slug-from-outline
qr-missing-shortener-detection
shownotes-publisher-omit-placeholder
shownotes-publisher-publish-no-date
shownotes-publisher-publish-with-date
shownotes-publisher-update-add-video
video-extraction-diagnostics
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
rules
scripts
skills
illustrations
presentation-creator
references
patterns
build
deliver
prepare
scripts
shownotes-publisher
vault-clarification
vault-ingress
vault-profile
tests