Diagram generation: ref-arch-driven procedure + spec validator + KB enrichment

The diagram path now follows a documented standard procedure (lookup
the closest Oracle Architecture Center reference → confirm components
→ author absolute_layout → spec validator → render → visually verify)
and ships persistent guardrails so layout regressions can't recur.

Persistent procedure changes (apply to all users, all sessions):
- tools/diagram_spec_validator.py — geometry checks (CONTAINER_TOO_THIN,
  CONTAINER_PADDING_VIOLATION, LABEL_OVERFLOW_PARENT) run BEFORE either
  renderer (drawio + PPTX). Catches the subnet-collapse / label-overflow
  bugs that the post-render drawio validator missed.
- tools/oci_diagram_gen.py + tools/oci_pptx_diagram_gen.py — call the
  spec validator before emitting any output. Adds mysql / mysql_heatwave
  type aliases.
- tools/archcenter_pattern_lookup.py — scores against cached page
  descriptions (not just the 1-line summary), supports --queries for
  multi-fragment composition, and applies synonym expansion via
  kb/architecture-center/synonyms.yaml so "LB HA cross AD" matches
  "load balancer high availability availability domain".
- kb/architecture-center/synonyms.yaml — canonical synonym table
  (load balancer, autonomous database, data guard, …) used by the
  lookup scorer.

KB enrichment:
- tools/archcenter_description_fetcher.py + 121 cached _description.md
  under kb/diagram/assets/archcenter-refs/<slug>/. Removes the runtime
  dependency on docs.oracle.com when authoring specs and feeds the
  pattern-lookup scorer.
- 110+ cached .drawio / .svg / .png references for offline reuse,
  plus the OCI Toolkit v24.2 import (kb/diagram/assets/oci-toolkit-drawio).

Documentation:
- docs/skill/output-formats.md — new "Standard diagram-generation
  procedure (MANDATORY)" + geometry rules + the new validator entry.
- SKILL.md option 2 — references the mandatory procedure.
- README.md — describes the spec validator, archcenter_pattern_lookup
  and description fetcher, and updates the KB-health table.

Tooling that backs the procedure (cumulative across recent sessions):
tools/archcenter_case_runner.py, archcenter_batch_driver.py,
archcenter_zip_downloader.py, drawio_visual_validator.py,
drawio_fidelity_eval.py, harvest_drawio_icon.py, import_oci_library.py,
oci_pptx_diagram_gen.py, oci_pptx_render.py, refresh_pptx_icon_index.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-25 21:15:21 -03:00
parent 2491c38d4b
commit b30a4f0d32
635 changed files with 365317 additions and 1014 deletions

View File

@@ -0,0 +1,365 @@
import base64
import sys
import zlib
from pathlib import Path
from urllib.parse import quote
from PIL import Image
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
from archcenter_case_runner import _canonical_service_label, _label_to_type, extract_layout
from drawio_visual_validator import validate_drawio
from oci_archcenter_batch import choose_zip_urls, process_case
from oci_diagram_gen import OCIDiagramGenerator
def test_extract_layout_recovers_service_from_layered_userobject_icon(tmp_path):
drawio = tmp_path / "layered.drawio"
drawio.write_text(
"""<mxfile host="app.diagrams.net">
<diagram id="d1" name="Page-1">
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<UserObject id="svc-base" label="">
<mxCell parent="1" style="shape=stencil(base);fillColor=#FFFFFF;strokeColor=none;" vertex="1">
<mxGeometry x="100" y="100" width="60" height="60" as="geometry"/>
</mxCell>
</UserObject>
<UserObject id="svc-top" label="">
<mxCell parent="1" style="shape=stencil(top);fillColor=#2D5967;strokeColor=none;" vertex="1">
<mxGeometry x="101" y="101" width="58" height="58" as="geometry"/>
</mxCell>
</UserObject>
<UserObject id="svc-label" label="Object Storage">
<mxCell parent="1" style="verticalAlign=middle;align=center;overflow=width;shape=stencil(label);fillColor=none;strokeColor=none;" vertex="1">
<mxGeometry x="82" y="168" width="95" height="20" as="geometry"/>
</mxCell>
</UserObject>
</root>
</mxGraphModel>
</diagram>
</mxfile>
""",
encoding="utf-8",
)
result = extract_layout(drawio)
assert len(result.services) == 1
assert result.services[0]["type"] == "object_storage"
assert result.icon_clusters_total == 1
assert result.icon_clusters_classified == 1
def test_choose_zip_urls_prefers_architecture_assets_over_functional_assets():
ordered = choose_zip_urls([
"https://docs.oracle.com/en/solutions/example/img/lakehouse-functional-oracle.zip",
"https://docs.oracle.com/en/solutions/example/img/lakehouse-architecture-oracle.zip",
"https://docs.oracle.com/en/solutions/example/img/lakehouse-overview-oracle.zip",
])
assert ordered[0].endswith("lakehouse-architecture-oracle.zip")
assert ordered[-1].endswith("lakehouse-overview-oracle.zip")
def test_label_to_type_normalizes_multiline_captions():
assert _label_to_type("Internet\ngateway") == "internet_gateway"
assert _label_to_type("Virtual\nMachine") == "virtual_machine"
assert _label_to_type("Oracle\nIntegration 3") == "integration"
def test_label_to_type_prefers_autonomous_database_when_integration_noise_leaks_in():
assert _label_to_type(
"OCI Region\nOracle\nIntegration 3\nAutonomous\nInstance\nDatabase"
) == "adb_d"
def test_canonical_service_label_drops_nearby_annotation_noise():
assert _canonical_service_label(
"drg",
'Match attachment "VCN-3"\nDRG\nVCN-4 Import Route Distribution',
) == "DRG"
assert _canonical_service_label(
"oracle_exadata_database_service",
"Oracle Services Network\nRecovery Service",
) == ""
def test_extract_layout_supports_compressed_drawio_payloads(tmp_path):
graph_xml = (
'<mxGraphModel><root>'
'<mxCell id="0"/>'
'<mxCell id="1" parent="0"/>'
'<UserObject id="svc" label="">'
'<mxCell parent="1" style="shape=stencil(icon);fillColor=#FFFFFF;strokeColor=none;" vertex="1">'
'<mxGeometry x="120" y="120" width="60" height="60" as="geometry"/>'
'</mxCell></UserObject>'
'<UserObject id="lbl" label="Internet Gateway">'
'<mxCell parent="1" style="verticalAlign=middle;align=center;overflow=width;shape=stencil(label);fillColor=none;strokeColor=none;" vertex="1">'
'<mxGeometry x="92" y="190" width="120" height="20" as="geometry"/>'
'</mxCell></UserObject>'
"</root></mxGraphModel>"
)
compressor = zlib.compressobj(level=9, wbits=-15)
compressed_bytes = compressor.compress(quote(graph_xml).encode("utf-8")) + compressor.flush()
compressed = base64.b64encode(compressed_bytes).decode("ascii")
drawio = tmp_path / "compressed.drawio"
drawio.write_text(
f'<mxfile host="app.diagrams.net"><diagram id="d1">{compressed}</diagram></mxfile>',
encoding="utf-8",
)
result = extract_layout(drawio)
assert len(result.services) == 1
assert result.services[0]["type"] == "internet_gateway"
def test_extract_layout_merges_nested_icon_satellite_cells(tmp_path):
drawio = tmp_path / "satellite.drawio"
drawio.write_text(
"""<mxfile host="app.diagrams.net">
<diagram id="d1" name="Page-1">
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<UserObject id="svc-base" label="">
<mxCell parent="1" style="shape=stencil(base);fillColor=#FFFFFF;strokeColor=none;" vertex="1">
<mxGeometry x="100" y="100" width="63" height="59" as="geometry"/>
</mxCell>
</UserObject>
<UserObject id="svc-overlay" label="">
<mxCell parent="1" style="shape=stencil(overlay);fillColor=#2D5967;strokeColor=none;" vertex="1">
<mxGeometry x="135" y="123" width="26" height="35" as="geometry"/>
</mxCell>
</UserObject>
<UserObject id="svc-label" label="Object Storage">
<mxCell parent="1" style="verticalAlign=middle;align=center;overflow=width;shape=stencil(label);fillColor=none;strokeColor=none;" vertex="1">
<mxGeometry x="88" y="168" width="95" height="20" as="geometry"/>
</mxCell>
</UserObject>
</root>
</mxGraphModel>
</diagram>
</mxfile>
""",
encoding="utf-8",
)
result = extract_layout(drawio)
assert len(result.services) == 1
assert result.services[0]["type"] == "object_storage"
assert len(result.services[0]["raw_icon_cells"]) == 2
def test_drawio_absolute_layout_preserves_raw_icon_cells(tmp_path):
spec = {
"absolute_layout": {
"canvas": {"width": 180, "height": 120},
"containers": [],
"services": [{
"id": "svc1",
"label": "",
"type": "custom_unknown",
"x": 24,
"y": 16,
"w": 60,
"h": 60,
"raw_icon_cells": [
'<mxCell id="10" parent="1" style="shape=stencil(custom_base);fillColor=#FFFFFF;strokeColor=none;" vertex="1"><mxGeometry x="0" y="0" width="60" height="60" as="geometry"/></mxCell>',
'<mxCell id="11" parent="1" style="shape=stencil(custom_overlay);fillColor=#2D5967;strokeColor=none;" vertex="1"><mxGeometry x="12" y="14" width="24" height="24" as="geometry"/></mxCell>',
],
}],
"labels": [],
"connections": [],
},
}
output = tmp_path / "raw-icon.drawio"
OCIDiagramGenerator.from_spec(spec).save(output)
xml = output.read_text(encoding="utf-8")
assert "shape=stencil(custom_base)" in xml
assert "shape=stencil(custom_overlay)" in xml
def test_drawio_absolute_layout_preserves_raw_underlay_cells_before_objects(tmp_path):
spec = {
"absolute_layout": {
"canvas": {"width": 180, "height": 120},
"containers": [],
"services": [],
"labels": [{
"id": "lbl1",
"text": "OCI Region",
"x": 60,
"y": 18,
"w": 70,
"h": 18,
"fontSize": 700,
}],
"raw_underlay_cells": [
'<mxCell id="u10" parent="1" style="shape=stencil(context_box);fillColor=#FFFFFF;strokeColor=none;" vertex="1"><mxGeometry x="6" y="4" width="36" height="18" as="geometry"/></mxCell>',
],
"connections": [],
},
}
output = tmp_path / "raw-underlay.drawio"
OCIDiagramGenerator.from_spec(spec).save(output)
xml = output.read_text(encoding="utf-8")
assert "shape=stencil(context_box)" in xml
assert xml.index("shape=stencil(context_box)") < xml.index('value="OCI Region"')
def test_validate_drawio_supports_compressed_payloads(tmp_path):
graph_xml = (
'<mxGraphModel><root>'
'<mxCell id="0"/>'
'<mxCell id="1" parent="0"/>'
'<UserObject id="svc" label="">'
'<mxCell parent="1" style="shape=stencil(icon);fontSize=14;" vertex="1">'
'<mxGeometry x="120" y="120" width="60" height="60" as="geometry"/>'
'</mxCell></UserObject>'
"</root></mxGraphModel>"
)
compressor = zlib.compressobj(level=9, wbits=-15)
compressed_bytes = compressor.compress(quote(graph_xml).encode("utf-8")) + compressor.flush()
compressed = base64.b64encode(compressed_bytes).decode("ascii")
drawio = tmp_path / "compressed.drawio"
drawio.write_text(
f'<mxfile host="app.diagrams.net"><diagram id="d1">{compressed}</diagram></mxfile>',
encoding="utf-8",
)
report = validate_drawio(drawio)
assert report["status"] == "pass"
assert report["cell_count"] > 0
assert report["issue_count"] == 0
def test_process_case_forwards_fidelity_threshold_and_requires_native_fidelity(tmp_path, monkeypatch):
output_root = tmp_path / "batch"
candidate = {
"entry": {
"title": "Example Architecture",
"url": "https://docs.oracle.com/en/solutions/example/index.html",
"date": "2026-04",
"tags": ["database"],
"services": ["exacs"],
}
}
reference_drawio = tmp_path / "reference.drawio"
reference_drawio.write_text(
'<mxfile><diagram id="d1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>',
encoding="utf-8",
)
reference_png = tmp_path / "reference.png"
Image.new("RGB", (32, 24), "white").save(reference_png)
reference_svg = tmp_path / "reference.svg"
reference_svg.write_text('<svg xmlns="http://www.w3.org/2000/svg"/>', encoding="utf-8")
def fake_stage_reference_assets(candidate_arg, case_dir):
(case_dir / "evidence").mkdir(parents=True, exist_ok=True)
return reference_drawio, reference_png, reference_svg, {"chosen_zip": "example.zip"}
captured: dict = {}
def fake_run_case(**kwargs):
captured.update(kwargs)
return {
"drawio": {
"rebuilt_path": "examples/out/example-rebuilt.drawio",
"status": "rebuilt+verbatim",
},
"pptx": {
"status": "ok",
"audit": {"unresolved_count": 0, "oversize_ref_count": 0},
},
"pptx_fidelity": {"status": "pass"},
"fidelity": {"status": "pass"},
"eval": {"status": "fail"},
"extraction": {"icon_clusters_total": 2, "icon_clusters_classified": 2},
}
monkeypatch.setattr("oci_archcenter_batch.stage_reference_assets", fake_stage_reference_assets)
monkeypatch.setattr("oci_archcenter_batch.run_case", fake_run_case)
result = process_case(candidate, 1, output_root, 0.82, 0.91)
assert captured["fidelity_threshold"] == 0.91
assert result["status"] == "PASS"
assert result["reasons"] == []
def fake_run_case_fail(**kwargs):
return {
"drawio": {
"rebuilt_path": "examples/out/example-rebuilt.drawio",
"status": "rebuilt+verbatim",
},
"pptx": {
"status": "ok",
"audit": {"unresolved_count": 0, "oversize_ref_count": 0},
},
"pptx_fidelity": {"status": "fail"},
"fidelity": {"status": "pass"},
"eval": {"status": "pass"},
"extraction": {"icon_clusters_total": 2, "icon_clusters_classified": 2},
}
monkeypatch.setattr("oci_archcenter_batch.run_case", fake_run_case_fail)
result_fail = process_case(candidate, 2, output_root, 0.82, 0.91)
assert result_fail["status"] == "FAIL"
assert "pptx_fidelity" in result_fail["reasons"]
def test_process_case_skips_cases_outside_native_benchmark_pool(tmp_path, monkeypatch):
output_root = tmp_path / "batch"
candidate = {
"entry": {
"title": "Learn about dynamic routing gateway solutions",
"url": "https://docs.oracle.com/en/solutions/example/index.html",
"date": "2026-04",
"tags": ["networking"],
"services": ["drg", "fastconnect"],
}
}
reference_drawio = tmp_path / "reference.drawio"
reference_drawio.write_text(
'<mxfile><diagram id="d1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>',
encoding="utf-8",
)
reference_png = tmp_path / "reference.png"
Image.new("RGB", (32, 24), "white").save(reference_png)
def fake_stage_reference_assets(candidate_arg, case_dir):
(case_dir / "evidence").mkdir(parents=True, exist_ok=True)
return reference_drawio, reference_png, None, {"chosen_zip": "example.zip"}
def fail_run_case(**kwargs):
raise AssertionError("run_case should not execute for skipped candidates")
monkeypatch.setattr("oci_archcenter_batch.stage_reference_assets", fake_stage_reference_assets)
monkeypatch.setattr(
"oci_archcenter_batch.assess_native_suitability",
lambda title, drawio_path: {"benchmarkable": False, "reasons": ["label_heavy"]},
)
monkeypatch.setattr("oci_archcenter_batch.run_case", fail_run_case)
result = process_case(candidate, 1, output_root, 0.82, 0.91)
assert result["status"] == "SKIP"
assert result["preflight"]["reasons"] == ["label_heavy"]

View File

@@ -81,3 +81,22 @@ def test_from_spec_accepts_roadmap_aliases_and_mixed_next_steps():
assert "Approve budget" in text
assert "Finance" in text
assert "May" in text
def test_from_spec_enriches_sparse_business_case_from_executive_summary():
spec = {
"customer_name": "Acme Corp",
"executive_summary": (
"Acme wants to move an on-prem Oracle platform to OCI to reduce cost "
"and improve disaster recovery in 12 weeks."
),
}
gen = BusinessCaseDeckGenerator.from_spec(spec)
text = _collect_slide_text(gen)
assert "Business Drivers" in text
assert "Risk Assessment" in text
assert "Implementation Roadmap" in text
assert "Our Recommendation" in text
assert "12 weeks" in text

View File

@@ -96,3 +96,28 @@ def test_from_spec_accepts_aliases_for_cost_migration_risk_scorecard_and_next_st
assert "Security" in text
assert "2/2" in text
assert "Validate scope" in text
def test_from_spec_enriches_sparse_nested_spec_with_safe_fallback_sections():
spec = {
"metadata": {
"customer": "Acme Corp",
"project": "Pilot",
},
"summary": {
"why": "Modernize core platform",
"current_state": ["Legacy Oracle DB on-prem"],
"target_state": "Target OCI platform with improved DR posture",
"timeline": "12 weeks",
},
}
gen = OCIDeckGenerator.from_spec(spec)
text = _collect_slide_text(gen)
assert "Architecture Overview" in text
assert "Migration Approach" in text
assert "Operational Responsibilities" in text
assert "Risk Register" in text
assert "Next Steps" in text
assert "12 weeks" in text

View File

@@ -74,3 +74,18 @@ def test_from_spec_accepts_source_target_and_omits_unresolved_connections():
gen = OCIDiagramGenerator.from_spec(spec)
assert gen._connection_count == 1
def test_drawio_icon_resolution_accepts_common_native_aliases():
gen = OCIDiagramGenerator()
db_icon, db_key = gen._resolve_icon_entry("database_system")
vm_icon, vm_key = gen._resolve_icon_entry("virtual_machine")
stream_icon, stream_key = gen._resolve_icon_entry("oci_streaming")
assert db_icon is not None
assert db_key in {"db_system", "dbcs"}
assert vm_icon is not None
assert vm_key in {"compute", "vm"}
assert stream_icon is not None
assert stream_key in {"streaming", "kafka"}

View File

@@ -0,0 +1,169 @@
import sys
import zipfile
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
from oci_deck_gen import OCIDeckGenerator
from oci_pptx_diagram_gen import NativePPTXDiagramRenderer
def test_native_pptx_architecture_slide_renders_into_saved_deck(tmp_path):
spec = {
"metadata": {
"customer": "TestCo",
"project": "Native Diagram",
"subtitle": "Smoke test",
},
"summary": {
"why": "Test native rendering",
"current_state": ["Legacy DB"],
"target_state": "OCI target",
"timeline": "Q2",
},
"architecture": {
"description": "Two-region native PPTX test",
"visual": {
"render_mode": "native_pptx",
"on_prem": {"name": "On-Premises", "connection": "FastConnect"},
"regions": [
{
"name": "Ashburn",
"label": "PRIMARY",
"availability_domains": [
{
"name": "Availability Domain 1",
"vcn": {
"name": "VCN",
"cidr": "10.10.0.0/16",
"subnets": [
{
"name": "App Subnet",
"cidr": "10.10.10.0/24",
"services": [
{"id": "lb", "name": "Load Balancer", "type": "load_balancer"},
{"id": "vm1", "name": "Virtual Machine", "type": "virtual_machine"},
{"id": "sgw", "name": "Service Gateway", "type": "service_gateway"},
],
},
{
"name": "Data Subnet",
"cidr": "10.10.20.0/24",
"services": [
{"id": "db", "name": "ADB-D", "type": "adb_d"},
{"id": "safe", "name": "Data Safe", "type": "data_safe"},
{"id": "obj", "name": "Object Storage", "type": "object_storage"},
],
},
],
},
},
{
"name": "Availability Domain 2",
"vcn": {
"name": "VCN",
"cidr": "10.10.0.0/16",
"subnets": [
{
"name": "Standby Subnet",
"cidr": "10.10.30.0/24",
"services": [
{"id": "dg", "name": "Data Guard", "type": "data_guard"},
],
},
],
},
},
],
},
{
"name": "Montreal",
"label": "DR STANDBY",
"services": [
{"id": "drdb", "name": "ADB-D", "type": "adb_d"},
{"id": "drdg", "name": "Data Guard", "type": "data_guard"},
],
"details": "Cross-region DR",
},
],
"connections": [
{"from": "lb", "to": "vm1", "label": "HTTPS"},
{"from": "vm1", "to": "db", "label": "SQL"},
{"from": "db", "to": "dg", "label": "ADG"},
{"from": "db", "to": "drdb", "label": "DR", "style": "dashed"},
],
"security_footer": "OCI-native smoke test footer",
},
},
}
output = tmp_path / "native-smoke-test.pptx"
gen = OCIDeckGenerator.from_spec(spec)
gen.save(str(output))
assert output.is_file()
with zipfile.ZipFile(output) as archive:
slide_xml = b"".join(
archive.read(name)
for name in archive.namelist()
if name.startswith("ppt/slides/slide") and name.endswith(".xml")
).decode("utf-8", errors="ignore")
rels_xml = b"".join(
archive.read(name)
for name in archive.namelist()
if name.startswith("ppt/slides/_rels/slide") and name.endswith(".rels")
).decode("utf-8", errors="ignore")
media_files = [
name for name in archive.namelist()
if name.startswith("ppt/media/")
]
assert "Availability Domain 1" in slide_xml
assert "FastConnect" in slide_xml
assert "OCI-native smoke test footer" in slide_xml
assert "Load" in slide_xml
assert "relationships/image" in rels_xml
assert media_files
def test_native_pptx_icon_resolution_accepts_common_aliases():
renderer = NativePPTXDiagramRenderer()
compute_ref = renderer._resolve_icon_ref(
{"name": "Virtual Machine app tier", "type": "compute"}
)
lb_ref = renderer._resolve_icon_ref(
{"name": "Ingress", "type": "public_load_balancer"}
)
storage_ref = renderer._resolve_icon_ref(
{"name": "Object Storage (DR)", "type": "storage"}
)
assert compute_ref is not None
assert compute_ref["display_number"] in {29, 30, 32, 34}
assert lb_ref is not None
assert "load" in lb_ref["text"].lower()
assert storage_ref is not None
assert "object" in storage_ref["text"].lower()
def test_native_pptx_icon_resolution_prefers_compact_refs_for_problem_icons():
renderer = NativePPTXDiagramRenderer()
api_ref = renderer._resolve_icon_ref({"name": "OCI API Gateway", "type": "api_gateway"})
notify_ref = renderer._resolve_icon_ref({"name": "Notifications", "type": "notifications"})
oke_ref = renderer._resolve_icon_ref({"name": "OKE cluster", "type": "oke"})
vpn_ref = renderer._resolve_icon_ref({"name": "Site-to-Site VPN", "type": "vpn"})
assert api_ref is not None
assert notify_ref is not None
assert oke_ref is not None
assert vpn_ref is not None
assert int(api_ref["bbox"]["cx"]) < 2_500_000
assert int(notify_ref["bbox"]["cx"]) < 2_500_000
assert int(oke_ref["bbox"]["cx"]) < 2_500_000

View File

@@ -0,0 +1,90 @@
import sys
from pathlib import Path
from PIL import Image
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
from oci_deck_gen import OCIDeckGenerator
from oci_pptx_render import render_pptx_to_png
def test_render_pptx_to_png_detects_native_architecture_slide(tmp_path):
spec = {
"metadata": {
"customer": "TestCo",
"project": "Native Diagram",
"subtitle": "Render test",
},
"summary": {
"why": "Test native rendering",
"current_state": ["Legacy DB"],
"target_state": "OCI target",
"timeline": "Q2",
},
"architecture": {
"description": "Absolute layout render test",
"visual": {
"render_mode": "native_pptx",
"absolute_layout": {
"canvas": {"width": 900, "height": 520},
"containers": [
{"id": "region", "type": "region", "label": "OCI Region", "x": 160, "y": 30, "w": 640, "h": 430},
{"id": "vcn", "type": "vcn", "label": "VCN 10.0.0.0/16", "x": 210, "y": 110, "w": 500, "h": 270},
],
"services": [
{"id": "api", "type": "api_gateway", "label": "", "x": 290, "y": 170, "w": 90, "h": 84},
{"id": "oke", "type": "oke", "label": "", "x": 420, "y": 165, "w": 96, "h": 88},
{"id": "notify", "type": "notifications", "label": "", "x": 560, "y": 170, "w": 90, "h": 84},
],
"labels": [
{"id": "lbl1", "text": "Coordinate-faithful reconstruction from the official Architecture Center diagram.", "x": 80, "y": 470, "w": 740, "h": 26},
],
"connections": [],
},
},
},
"output": {"render_standard_sections": False},
"custom_slides": [{
"type": "diagram",
"title": "Render Test",
"description": "Coordinate-faithful reconstruction from the official Architecture Center diagram.",
"visual": {
"render_mode": "native_pptx",
"absolute_layout": {
"canvas": {"width": 900, "height": 520},
"containers": [
{"id": "region", "type": "region", "label": "OCI Region", "x": 160, "y": 30, "w": 640, "h": 430},
],
"services": [
{"id": "api", "type": "api_gateway", "label": "", "x": 320, "y": 170, "w": 90, "h": 84},
{"id": "notify", "type": "notifications", "label": "", "x": 480, "y": 170, "w": 90, "h": 84},
],
"labels": [
{"id": "lbl1", "text": "Coordinate-faithful reconstruction from the official Architecture Center diagram.", "x": 90, "y": 470, "w": 720, "h": 26},
],
"connections": [],
},
},
}],
}
pptx_path = tmp_path / "render-test.pptx"
png_path = tmp_path / "render-test.png"
gen = OCIDeckGenerator.from_spec(spec)
gen.save(str(pptx_path))
meta = render_pptx_to_png(pptx_path, png_path, width=1200)
assert png_path.is_file()
assert meta["display_number"] >= 1
assert meta["shape_counts"]["grpSp"] >= 2
with Image.open(png_path) as image:
assert image.size[0] == 1200
assert image.getbbox() is not None

View File

@@ -0,0 +1,68 @@
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
if str(TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(TOOLS_DIR))
from refresh_pptx_icon_index import DEFAULT_CONFIG_PATH, build_outputs, load_config
def test_build_outputs_detects_semantic_slides_for_bundled_oci_icons():
config = load_config(DEFAULT_CONFIG_PATH)
asset = PROJECT_ROOT / config["asset"]["bundled_path"]
index, manifest = build_outputs(asset, config)
assert asset.is_file()
assert index["library"]["slide_count"] >= 40
assert "connectors_logical" in index["semantic_slides"]
assert "grouping" in index["semantic_slides"]
assert "database_icons" in index["semantic_slides"]
assert len(index["slide_catalog"]) == index["library"]["slide_count"]
assert index["instruction_catalog"]["slide_count_with_instructions"] >= 10
assert manifest["asset"]["sha256"] == index["library"]["sha256"]
def test_build_outputs_extracts_expected_database_icon_refs():
config = load_config(DEFAULT_CONFIG_PATH)
asset = PROJECT_ROOT / config["asset"]["bundled_path"]
index, _ = build_outputs(asset, config)
icons = index["stencils"]["database_icons"]
assert "adb_d" in icons
assert "atp_d" in icons
assert "adw_d" in icons
assert "data_guard" in icons
def test_build_outputs_indexes_full_slide_and_shape_library():
config = load_config(DEFAULT_CONFIG_PATH)
asset = PROJECT_ROOT / config["asset"]["bundled_path"]
index, manifest = build_outputs(asset, config)
slide_catalog = index["slide_catalog"]
instruction_slides = {
slide["display_number"]: slide
for slide in index["instruction_catalog"]["slides"]
}
shape_library = index["shape_library"]["entries"]
assert slide_catalog[0]["display_number"] == 1
assert slide_catalog[-1]["display_number"] == index["library"]["slide_count"]
assert 10 in instruction_slides
assert 18 in instruction_slides
assert 43 in instruction_slides
assert any("Open Arrowhead style" in line for line in instruction_slides[10]["instruction_lines"])
assert any("OCI Region" in line for line in instruction_slides[18]["instruction_lines"])
assert "adb_d" in shape_library
assert "data_guard" in shape_library
assert "api_gateway" in shape_library
assert "notifications" in shape_library
assert "oci_container_engine_for_kubernetes" in shape_library
assert any(ref.get("node_path") and len(ref["node_path"]) > 1 for ref in shape_library["api_gateway"])
assert manifest["coverage"]["catalogued_slides"] == index["library"]["slide_count"]