From 6b5c3a2609e69be294d1eb49a13eee19bbde1182 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 21 Jun 2026 15:34:39 -0400 Subject: [PATCH] Strengthen php-transformer contract fixtures --- ...json => html-expanded-gap-primitives.json} | 4 +- .../site-artifact-report-consistency.json | 129 ++++++++++++++++++ php-transformer/tests/parity/run.php | 62 +++++++++ 3 files changed, 193 insertions(+), 2 deletions(-) rename php-transformer/tests/fixtures/parity/{html-h2bc-gap-primitives.json => html-expanded-gap-primitives.json} (98%) create mode 100644 php-transformer/tests/fixtures/parity/site-artifact-report-consistency.json diff --git a/php-transformer/tests/fixtures/parity/html-h2bc-gap-primitives.json b/php-transformer/tests/fixtures/parity/html-expanded-gap-primitives.json similarity index 98% rename from php-transformer/tests/fixtures/parity/html-h2bc-gap-primitives.json rename to php-transformer/tests/fixtures/parity/html-expanded-gap-primitives.json index 1d7964a..c973747 100644 --- a/php-transformer/tests/fixtures/parity/html-h2bc-gap-primitives.json +++ b/php-transformer/tests/fixtures/parity/html-expanded-gap-primitives.json @@ -1,10 +1,10 @@ { "schema": "blocks-engine/php-transformer/parity-fixture/v1", - "name": "html-h2bc-gap-primitives", + "name": "html-expanded-gap-primitives", "description": "Preserves generic HTML parity gaps for visual placeholders, media wrappers, nested lists, scoped runtime fallbacks, resized SVG images, and code-window labels.", "source_reference": { "repo": "php-transformer", - "path": "tests/fixtures/parity/html-h2bc-gap-primitives.json", + "path": "tests/fixtures/parity/html-expanded-gap-primitives.json", "notes": "Product-neutral fixture for reusable upstream HTML primitives inspired by downstream parity gaps." }, "legacy_comparison": { diff --git a/php-transformer/tests/fixtures/parity/site-artifact-report-consistency.json b/php-transformer/tests/fixtures/parity/site-artifact-report-consistency.json new file mode 100644 index 0000000..b5edf34 --- /dev/null +++ b/php-transformer/tests/fixtures/parity/site-artifact-report-consistency.json @@ -0,0 +1,129 @@ +{ + "schema": "blocks-engine/php-transformer/parity-fixture/v1", + "name": "site-artifact-report-consistency", + "description": "Compiles a mixed generic site artifact and verifies that source reports, materialization plans, conversion summaries, local references, and rejected inputs stay aligned.", + "source_reference": { + "repo": "php-transformer", + "path": "tests/fixtures/parity/site-artifact-report-consistency.json", + "notes": "Product-neutral fixture for downstream handoff contracts around site artifacts and report consistency." + }, + "legacy_comparison": { + "skip": true, + "reason": "Report consistency assertions cover the canonical transformer contract rather than a legacy package shape." + }, + "operation": "artifact_compiler.compile", + "input": { + "artifact": { + "schema": "blocks-engine/php-transformer/site-artifact/v1", + "entrypoint": "public/index.html", + "files": [ + { + "path": "public/index.html", + "content": "

Contract Site

Entry page.

\"Hero\"
", + "mime_type": "text/html", + "role": "entry" + }, + { + "path": "public/docs/guide.html", + "content": "

Guide block markup.

", + "mime_type": "text/html" + }, + { + "path": "public/blog/post.html", + "content": "

Post

Article page.

", + "mime_type": "text/html" + }, + { + "path": "public/parts/header.html", + "content": "
\"Header
", + "mime_type": "text/html", + "role": "template-part" + }, + { + "path": "content/notes.md", + "content": "---\ntitle: Release Notes\nslug: release-notes\n---\n# Notes\n\nMarkdown page.", + "mime_type": "text/markdown" + }, + { + "path": "public/assets/app.css", + "content": "body{background:url('texture.png')} main{max-width:70rem}", + "mime_type": "text/css", + "role": "stylesheet" + }, + { + "path": "public/assets/visual-repair.css", + "content": ".wp-site-blocks{min-height:100vh}", + "mime_type": "text/css", + "intent": "visual-repair" + }, + { + "path": "public/assets/hero.png", + "content_base64": "iVBORw0KGgo=", + "mime_type": "image/png", + "role": "image" + }, + { + "path": "public/assets/hero@2x.png", + "content_base64": "iVBORw0KGgo=", + "mime_type": "image/png", + "role": "image" + }, + { + "path": "public/assets/texture.png", + "content_base64": "iVBORw0KGgo=", + "mime_type": "image/png", + "role": "image" + }, + { + "path": "../outside.html", + "content": "
Rejected
", + "mime_type": "text/html" + } + ] + } + }, + "expect": [ + { "path": "schema", "assert": "equals", "value": "blocks-engine/php-transformer/result/v1" }, + { "path": "status", "assert": "equals", "value": "success_with_warnings" }, + { "path": "source_reports.artifact.schema", "assert": "equals", "value": "blocks-engine/php-transformer/site-artifact/v1" }, + { "path": "source_reports.artifact.original_schema", "assert": "equals", "value": "blocks-engine/php-transformer/site-artifact/v1" }, + { "path": "source_reports.artifact.entry_path", "assert": "equals", "value": "public/index.html" }, + { "path": "source_reports.artifact.rejected_count", "assert": "equals", "value": 1 }, + { "path": "source_reports.artifact.files_by_kind.html", "assert": "equals", "value": 4 }, + { "path": "source_reports.artifact.files_by_kind.markdown", "assert": "equals", "value": 1 }, + { "path": "source_reports.artifact.files_by_mime.image/png", "assert": "equals", "value": 3 }, + { "path": "source_reports.artifact.internal_links", "assert": "count", "count": 0 }, + { "path": "source_reports.artifact.asset_references", "assert": "count", "count": 5 }, + { "path": "source_reports.compiled_site.pages", "assert": "count", "count": 4 }, + { "path": "source_reports.compiled_site.pages.1.block_markup", "assert": "contains", "value": "" }, + { "path": "source_reports.compiled_site.pages.3.title", "assert": "equals", "value": "Release Notes" }, + { "path": "source_reports.materialization_plan.pages", "assert": "count", "count": 4 }, + { "path": "source_reports.materialization_plan.routes", "assert": "count", "count": 4 }, + { "path": "source_reports.materialization_plan.routes.0.target_path", "assert": "equals", "value": "/" }, + { "path": "source_reports.materialization_plan.routes.1.target_path", "assert": "equals", "value": "/guide" }, + { "path": "source_reports.materialization_plan.routes.2.target_path", "assert": "equals", "value": "/post" }, + { "path": "source_reports.materialization_plan.routes.3.target_path", "assert": "equals", "value": "/release-notes" }, + { "path": "source_reports.materialization_plan.navigation_links", "assert": "count", "count": 3 }, + { "path": "source_reports.materialization_plan.navigation_links.1.target_path", "assert": "equals", "value": "/docs/guide" }, + { "path": "source_reports.materialization_plan.navigation_links.2.target_path", "assert": "equals", "value": "/blog/post" }, + { "path": "source_reports.materialization_plan.navigation_links.2.target_slug", "assert": "equals", "value": "post" }, + { "path": "source_reports.materialization_plan.menus.0.items", "assert": "equals", "value": 3 }, + { "path": "source_reports.materialization_plan.template_part_writes.0.type", "assert": "equals", "value": "wp_template_part" }, + { "path": "source_reports.materialization_plan.assets", "assert": "count", "count": 9 }, + { "path": "source_reports.materialization_plan.asset_rewrite_candidates", "assert": "count", "count": 0 }, + { "path": "source_reports.materialization_plan.theme.stylesheets.0", "assert": "equals", "value": "public/assets/app.css" }, + { "path": "source_reports.materialization_plan.theme.images", "assert": "count", "count": 3 }, + { "path": "source_reports.materialization_plan.visual_repair_css", "assert": "contains", "value": "min-height:100vh" }, + { "path": "source_reports.materialization_plan.totals.pages", "assert": "equals", "value": 4 }, + { "path": "source_reports.materialization_plan.totals.assets", "assert": "equals", "value": 9 }, + { "path": "source_reports.conversion_report.source_summary.page_count", "assert": "equals", "value": 4 }, + { "path": "source_reports.conversion_report.source_summary.asset_count", "assert": "equals", "value": 9 }, + { "path": "source_reports.conversion_report.source_summary.route_count", "assert": "equals", "value": 4 }, + { "path": "source_reports.conversion_report.source_summary.navigation_link_count", "assert": "equals", "value": 3 }, + { "path": "source_reports.conversion_report.source_summary.menu_count", "assert": "equals", "value": 1 }, + { "path": "source_reports.conversion_report.asset_refs", "assert": "count", "count": 5 }, + { "path": "source_reports.conversion_report.presentation_gaps.0.path", "assert": "equals", "value": "public/assets/visual-repair.css" }, + { "path": "documents.0.source_path", "assert": "equals", "value": "content/notes.md" }, + { "path": "diagnostics.0.code", "assert": "equals", "value": "unsafe_artifact_path" } + ] +} diff --git a/php-transformer/tests/parity/run.php b/php-transformer/tests/parity/run.php index 67060a5..6383d70 100644 --- a/php-transformer/tests/parity/run.php +++ b/php-transformer/tests/parity/run.php @@ -64,6 +64,7 @@ function serialize_blocks(array $blocks): string assertExpectation($output, $expectation, $fixture['name']); } assertStructuredCoverage($output, $fixture); + assertArtifactReportConsistency($output, $fixture); if ( $runMigrationComparisons ) { $migrationResult = runLegacyComparison($fixture, $output); @@ -85,6 +86,67 @@ function serialize_blocks(array $blocks): string fwrite(STDOUT, "PHP transformer parity fixtures passed: {$ran} fixture(s).\n"); } +/** + * @param array $output + * @param array $fixture + */ +function assertArtifactReportConsistency(array $output, array $fixture): void +{ + if ( 'artifact_compiler.compile' !== ($fixture['operation'] ?? '') ) { + return; + } + + $sourceReports = $output['source_reports'] ?? array(); + if ( ! is_array($sourceReports) ) { + fail("Fixture {$fixture['name']} output source_reports must be an object."); + } + + $materializationPlan = $sourceReports['materialization_plan'] ?? array(); + $conversionReport = $sourceReports['conversion_report'] ?? array(); + if ( ! is_array($materializationPlan) || ! is_array($conversionReport) ) { + fail("Fixture {$fixture['name']} artifact output must include materialization and conversion reports."); + } + + $totals = $materializationPlan['totals'] ?? array(); + $summary = $conversionReport['source_summary'] ?? array(); + if ( ! is_array($totals) || ! is_array($summary) ) { + fail("Fixture {$fixture['name']} materialization totals and conversion source summary must be objects."); + } + + $summaryKeys = array( + 'pages' => 'page_count', + 'assets' => 'asset_count', + 'routes' => 'route_count', + 'navigation_links' => 'navigation_link_count', + 'menus' => 'menu_count', + ); + foreach ( $summaryKeys as $totalKey => $summaryKey ) { + if ( ($totals[$totalKey] ?? null) !== ($summary[$summaryKey] ?? null) ) { + failExpectation((string) $fixture['name'], "source_reports.conversion_report.source_summary.{$summaryKey}", $totals[$totalKey] ?? null, $summary[$summaryKey] ?? null); + } + } + + foreach ( array('pages', 'routes', 'navigation_links', 'menus', 'template_parts', 'template_part_writes', 'assets') as $listKey ) { + if ( ! array_key_exists($listKey, $materializationPlan) || ! is_array($materializationPlan[$listKey]) ) { + fail("Fixture {$fixture['name']} materialization_plan.{$listKey} must be an array."); + } + } + + $countedTotals = array( + 'pages' => count($materializationPlan['pages']), + 'routes' => count($materializationPlan['routes']), + 'navigation_links' => count($materializationPlan['navigation_links']), + 'menus' => count($materializationPlan['menus']), + 'template_parts' => count($materializationPlan['template_parts']), + 'assets' => count($materializationPlan['assets']), + ); + foreach ( $countedTotals as $totalKey => $count ) { + if ( $count !== ($totals[$totalKey] ?? null) ) { + failExpectation((string) $fixture['name'], "source_reports.materialization_plan.totals.{$totalKey}", $count, $totals[$totalKey] ?? null); + } + } +} + /** * @param array $output * @param array $fixture