nexus-ai / tests /test_layout_coverage.py
nexus-deploy
deploy: sync backend b07a8d5
9c3ba60
Raw
History Blame Contribute Delete
11.3 kB
"""Phase 1A regression tests: canonical layout coverage.
These tests guard the contract that the backend agent's notion of "valid
layouts" is exactly what the canonical registry says, and that every
canonical layout survives ``NexusAgentLoop._normalize_slides`` instead of
being silently rewritten to ``bullets`` (or anything else).
They intentionally avoid pulling in ``backend.tests.conftest`` /
``database.connection`` so they can be exercised in isolation by anyone
running ``python -m pytest --noconftest backend/tests/test_layout_coverage.py``.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
# Ensure ``backend/`` is on sys.path when this file is invoked directly
# (e.g. via ``python -m pytest`` from the repo root) so the ``agent``
# package resolves the same way it does inside the container.
_BACKEND_ROOT = Path(__file__).resolve().parent.parent
if str(_BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(_BACKEND_ROOT))
from agent.layouts_registry import ( # noqa: E402
CANONICAL_LAYOUTS,
FALLBACK_LAYOUT,
LAYOUT_ALIASES,
normalize_layout,
)
from agent.loop import NexusAgentLoop, _VALID_LAYOUTS # noqa: E402
from agent.planner import Planner, _VALID_LAYOUTS as PLANNER_VALID_LAYOUTS # noqa: E402
_REGISTRY_JSON = _BACKEND_ROOT / "agent" / "layouts.registry.json"
_FRONTEND_REGISTRY_JSON = (
_BACKEND_ROOT.parent / "frontend" / "src" / "design" / "layouts.registry.json"
)
def _load_registry_names(path: Path) -> list[str]:
with path.open("r", encoding="utf-8") as fh:
data = json.load(fh)
return [entry["name"] for entry in data["layouts"]]
# --------------------------------------------------------------------------
# Registry parity
# --------------------------------------------------------------------------
def test_backend_valid_layouts_matches_registry() -> None:
"""The backend's _VALID_LAYOUTS set must equal the canonical registry."""
registry_names = _load_registry_names(_REGISTRY_JSON)
assert set(_VALID_LAYOUTS) == set(registry_names), (
"_VALID_LAYOUTS drifted from layouts.registry.json"
)
assert set(_VALID_LAYOUTS) == set(CANONICAL_LAYOUTS), (
"_VALID_LAYOUTS drifted from CANONICAL_LAYOUTS"
)
def test_backend_and_frontend_registry_files_match() -> None:
"""The two registry copies (backend + frontend) must stay byte-aligned.
When the backend test runs in a container that does not mount the
frontend tree (the default in this repo's docker-compose), this check
is skipped because the JS-side ``scripts/verify-layouts.mjs`` already
enforces the same parity from the frontend side.
"""
if not _FRONTEND_REGISTRY_JSON.exists():
pytest.skip(
"frontend registry not reachable from backend test runtime; "
"parity is enforced by scripts/verify-layouts.mjs instead."
)
backend_data = json.loads(_REGISTRY_JSON.read_text(encoding="utf-8"))
frontend_data = json.loads(_FRONTEND_REGISTRY_JSON.read_text(encoding="utf-8"))
assert backend_data == frontend_data, (
"backend/agent/layouts.registry.json and "
"frontend/src/design/layouts.registry.json have drifted"
)
def test_fallback_layout_is_canonical() -> None:
assert FALLBACK_LAYOUT in CANONICAL_LAYOUTS
def test_aliases_only_target_canonical_layouts() -> None:
for src, target in LAYOUT_ALIASES.items():
assert target in CANONICAL_LAYOUTS, (
f"alias {src!r} -> {target!r} but {target!r} is not canonical"
)
# --------------------------------------------------------------------------
# Round-trip per canonical layout
# --------------------------------------------------------------------------
def _seed_slide(layout: str, idx: int) -> dict:
"""Build a minimal slide payload that should survive normalization."""
if layout == "title":
return {
"layout": "title",
"title": f"Title {idx}",
"subtitle": "Sub",
"eyebrow": "Eye",
}
if layout == "bullets":
return {
"layout": "bullets",
"title": f"Bullets {idx}",
"bullets": ["a", "b", "c"],
}
if layout == "two-col":
return {
"layout": "two-col",
"title": f"Two-col {idx}",
"columns": [
{"heading": "L", "body": "left body"},
{"heading": "R", "body": "right body"},
],
}
if layout == "quote":
return {
"layout": "quote",
"title": f"Quote {idx}",
"quote": "To be or not to be",
"attribution": "Shakespeare",
}
if layout == "stats":
return {
"layout": "stats",
"title": f"Stats {idx}",
"stats": [
{"value": "42", "label": "answer"},
{"value": "7", "label": "wonders"},
],
}
if layout == "chart":
return {
"layout": "chart",
"title": f"Chart {idx}",
"chart_type": "bar",
"chart_data": {
"labels": ["A", "B", "C"],
"values": [1, 2, 3],
"unit": "k",
"source": "test",
},
}
if layout == "closing":
return {
"layout": "closing",
"title": f"Closing {idx}",
"subtitle": "Thanks",
"cta": "Questions?",
}
# Phase 6AA — single dominant metric.
if layout == "bigstat":
return {
"layout": "bigstat",
"title": f"Bigstat {idx}",
"value": "93%",
"label": "Adoption",
"subtitle": "Across the cohort",
}
# Phase 6AA — typography pause.
if layout == "section_divider":
return {
"layout": "section_divider",
"title": f"Part {idx}",
"eyebrow": "Continued",
"subtitle": "Implications",
}
# Phase 6AC — chronology of dated events.
if layout == "timeline":
return {
"layout": "timeline",
"title": f"Timeline {idx}",
"subtitle": "",
"events": [
{"date": "1980", "label": "Event A"},
{"date": "1985", "label": "Event B"},
{"date": "1990", "label": "Event C"},
],
}
# Phase 6AC — left/right contrast.
if layout == "comparison":
return {
"layout": "comparison",
"title": f"Comparison {idx}",
"subtitle": "",
"left": {"heading": "Before", "body": "Manual workflows."},
"right": {"heading": "After", "body": "Automated pipeline."},
}
raise AssertionError(f"unknown canonical layout: {layout}")
@pytest.mark.parametrize("layout", list(CANONICAL_LAYOUTS))
def test_normalize_preserves_canonical_layout(layout: str) -> None:
"""Every canonical layout must survive _normalize_slides when present.
The normalizer pins slide[0]=title and slide[-1]=closing and also
auto-promotes a stats slide to chart when no chart is present, so we
seed a chart slide alongside non-edge layouts to exercise the
layout-preservation contract specifically.
"""
if layout == "title":
slides = [
_seed_slide("title", 0),
_seed_slide("bullets", 1),
_seed_slide("chart", 2),
_seed_slide("closing", 3),
]
normalized = NexusAgentLoop._normalize_slides(slides, len(slides), "topic")
assert normalized[0]["layout"] == "title"
return
if layout == "closing":
slides = [
_seed_slide("title", 0),
_seed_slide("bullets", 1),
_seed_slide("chart", 2),
_seed_slide("closing", 3),
]
normalized = NexusAgentLoop._normalize_slides(slides, len(slides), "topic")
assert normalized[-1]["layout"] == "closing"
return
# Always include a chart sibling so the loop's stats->chart
# auto-promotion does not interfere with the layout under test.
middle = [_seed_slide(layout, 1)]
if layout != "chart":
middle.append(_seed_slide("chart", 2))
slides = [_seed_slide("title", 0), *middle, _seed_slide("closing", 99)]
normalized = NexusAgentLoop._normalize_slides(slides, len(slides), "topic")
assert len(normalized) == len(slides)
assert normalized[1]["layout"] == layout, (
f"layout {layout!r} was rewritten to {normalized[1]['layout']!r}"
)
def test_unknown_layout_falls_back_to_fallback_not_silently_dropped() -> None:
slides = [
_seed_slide("title", 0),
{"layout": "totally-not-a-real-layout", "title": "X"},
_seed_slide("chart", 2),
_seed_slide("closing", 3),
]
normalized = NexusAgentLoop._normalize_slides(slides, len(slides), "topic")
assert normalized[1]["layout"] == FALLBACK_LAYOUT
# And the registry's normalize_layout helper agrees:
assert normalize_layout("totally-not-a-real-layout") == FALLBACK_LAYOUT
def test_normalize_layout_canonical_passthrough() -> None:
for name in CANONICAL_LAYOUTS:
assert normalize_layout(name) == name
assert normalize_layout(name.upper()) == name
assert normalize_layout(f" {name} ") == name
# --------------------------------------------------------------------------
# Planner round-trip — Phase 1A.1
# --------------------------------------------------------------------------
def test_planner_valid_layouts_matches_registry() -> None:
"""Planner._VALID_LAYOUTS must equal the canonical registry.
This guards against the Phase 1A.1 drift where ``planner.py`` carried
its own hardcoded set that was missing ``chart``.
"""
assert set(PLANNER_VALID_LAYOUTS) == set(CANONICAL_LAYOUTS)
def _planner_outline_json(layout: str) -> str:
"""Build a single-item JSON outline that Planner._parse_outline accepts."""
return json.dumps(
[
{
"layout": layout,
"title": f"{layout} title",
"intent": f"intent for {layout}",
}
]
)
@pytest.mark.parametrize("layout", list(CANONICAL_LAYOUTS))
def test_planner_preserves_canonical_layout(layout: str) -> None:
"""Every canonical layout (including ``chart``) must survive
``Planner._parse_outline`` instead of being collapsed to ``bullets``.
"""
parsed = Planner._parse_outline(_planner_outline_json(layout))
assert len(parsed) == 1
assert parsed[0]["layout"] == layout, (
f"planner rewrote {layout!r} -> {parsed[0]['layout']!r}"
)
def test_planner_unknown_layout_falls_back_to_fallback_layout() -> None:
parsed = Planner._parse_outline(
_planner_outline_json("definitely-not-a-real-layout")
)
assert len(parsed) == 1
assert parsed[0]["layout"] == FALLBACK_LAYOUT
def test_planner_chart_layout_not_lost() -> None:
"""Regression test for the Phase 1A.1 drift: ``chart`` was missing from
the planner's hardcoded whitelist. It must now pass through.
"""
parsed = Planner._parse_outline(_planner_outline_json("chart"))
assert parsed[0]["layout"] == "chart"