From e1f3dd364ce3d00e9420ada1b7340b5cfa532931 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Sun, 14 Jun 2026 23:02:52 -0400 Subject: [PATCH] test(output-prompt-conformance): deep nested request-VO + depth-guard fallback cross-port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase-4 conformance: gate the FR-010 output-format prompt fragment for LARGE, deeply-nested request payloads — the existing corpus only had a 3-level pure-singular case (json-deep-nest) and shallow arrays. Two new cases: - json-deep-mixed (roundTrip:true): a realistic 4-level request VO mixing every nesting shape in one spec — singular objects 4 deep (payload.author.contact.{email,verified}), an array-of-objects (payload.sections[]), a nested enum + boolean, and an optional-with-example. Gates guide dotted-path recursion at depth, four-level indentation, unquoted boolean / enum rendering, and (roundTrip) that extract reads the whole graph back with no MALFORMED/LOST_*. - json-depth-guard-fallback (roundTrip:false): a 9-level chain that drives the renderer past MAX_NEST_DEPTH (8). At depth 8 the guard emits the flat {n9} placeholder instead of recursing into n9's child — pinning that the renderer never recurses unboundedly, byte-identically across ports (all share MAX_NEST_DEPTH = 8). Goldens snapshot-generated by the renderer, reviewed for correctness, then gated. Verified byte-identical in all five ports: TS 14 / Python 15 / C# 15 / Java 14 / Kotlin (BUILD SUCCESS). The EXPECTED_CASE_COUNT guard bumps 12 → 14 in all five. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../json-deep-mixed/README.md | 17 +++++++ .../json-deep-mixed/expected.exampleOnly.txt | 20 +++++++++ .../json-deep-mixed/expected.guide.txt | 44 +++++++++++++++++++ .../json-deep-mixed/expected.inline.txt | 20 +++++++++ .../json-deep-mixed/spec.json | 27 ++++++++++++ .../json-depth-guard-fallback/README.md | 11 +++++ .../expected.exampleOnly.txt | 19 ++++++++ .../expected.guide.txt | 31 +++++++++++++ .../expected.inline.txt | 19 ++++++++ .../json-depth-guard-fallback/spec.json | 32 ++++++++++++++ .../OutputPromptConformanceTests.cs | 2 +- .../kotlin/OutputPromptConformanceTest.kt | 2 +- .../prompt/OutputPromptConformanceTest.java | 2 +- .../render/test_output_prompt_conformance.py | 2 +- .../test/output-prompt-conformance.test.ts | 2 +- 15 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 fixtures/output-prompt-conformance/json-deep-mixed/README.md create mode 100644 fixtures/output-prompt-conformance/json-deep-mixed/expected.exampleOnly.txt create mode 100644 fixtures/output-prompt-conformance/json-deep-mixed/expected.guide.txt create mode 100644 fixtures/output-prompt-conformance/json-deep-mixed/expected.inline.txt create mode 100644 fixtures/output-prompt-conformance/json-deep-mixed/spec.json create mode 100644 fixtures/output-prompt-conformance/json-depth-guard-fallback/README.md create mode 100644 fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.exampleOnly.txt create mode 100644 fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.guide.txt create mode 100644 fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.inline.txt create mode 100644 fixtures/output-prompt-conformance/json-depth-guard-fallback/spec.json diff --git a/fixtures/output-prompt-conformance/json-deep-mixed/README.md b/fixtures/output-prompt-conformance/json-deep-mixed/README.md new file mode 100644 index 000000000..9a359916a --- /dev/null +++ b/fixtures/output-prompt-conformance/json-deep-mixed/README.md @@ -0,0 +1,17 @@ +# json-deep-mixed + +A large, realistic **request VO** four levels deep that mixes every nesting shape +in one spec — the cross-port gate for deep prompt-payload rendering: + +- **singular nested objects** 4 levels deep: `payload.author.contact.{email,verified}` +- an **array of objects**: `payload.sections[]` with `heading` + `priority` +- a nested **enum** (`priority`: `LOW | HIGH`) and a **boolean** (`verified`) +- an **optional** field carrying an example (`payload.summary`) so the whole thing + still round-trips. + +Exercises: guide dotted-path recursion at depth (`payload.author.contact.email`, +`payload.sections[].priority`), indentation at four levels, unquoted boolean +example (`true`), enum `one of …` (guide) / `LOW | HIGH` (inline), and JSON +array-of-objects rendered as `[ one element ]`. `roundTrip: true` — every field +carries a real example, so `extract(exampleOnly)` reads the whole graph back with +no MALFORMED / LOST_* states. diff --git a/fixtures/output-prompt-conformance/json-deep-mixed/expected.exampleOnly.txt b/fixtures/output-prompt-conformance/json-deep-mixed/expected.exampleOnly.txt new file mode 100644 index 000000000..8d1342a79 --- /dev/null +++ b/fixtures/output-prompt-conformance/json-deep-mixed/expected.exampleOnly.txt @@ -0,0 +1,20 @@ +{ + "requestId": "req-42", + "payload": { + "title": "Spring Sale", + "author": { + "name": "Ada", + "contact": { + "email": "ada@example.com", + "verified": true + } + }, + "sections": [ + { + "heading": "Intro", + "priority": "LOW" + } + ], + "summary": "All good" + } +} \ No newline at end of file diff --git a/fixtures/output-prompt-conformance/json-deep-mixed/expected.guide.txt b/fixtures/output-prompt-conformance/json-deep-mixed/expected.guide.txt new file mode 100644 index 000000000..08382a885 --- /dev/null +++ b/fixtures/output-prompt-conformance/json-deep-mixed/expected.guide.txt @@ -0,0 +1,44 @@ +Fill in each field as described below: +- requestId (required): client request id + e.g. req-42 +- payload (required) +- payload.title (required): document title + e.g. Spring Sale +- payload.author (required) +- payload.author.name (required): author name + e.g. Ada +- payload.author.contact (required) +- payload.author.contact.email (required): reply address + e.g. ada@example.com +- payload.author.contact.verified (required): is the address verified + e.g. true +- payload.sections (required) +- payload.sections[].heading (required): section heading + e.g. Intro +- payload.sections[].priority (required): render priority + one of LOW, HIGH + e.g. LOW +- payload.summary (optional): optional one-line summary + e.g. All good + +Respond exactly like this: +{ + "requestId": "req-42", + "payload": { + "title": "Spring Sale", + "author": { + "name": "Ada", + "contact": { + "email": "ada@example.com", + "verified": true + } + }, + "sections": [ + { + "heading": "Intro", + "priority": "LOW" + } + ], + "summary": "All good" + } +} \ No newline at end of file diff --git a/fixtures/output-prompt-conformance/json-deep-mixed/expected.inline.txt b/fixtures/output-prompt-conformance/json-deep-mixed/expected.inline.txt new file mode 100644 index 000000000..70c022fef --- /dev/null +++ b/fixtures/output-prompt-conformance/json-deep-mixed/expected.inline.txt @@ -0,0 +1,20 @@ +{ + "requestId": "{client request id}", + "payload": { + "title": "{document title}", + "author": { + "name": "{author name}", + "contact": { + "email": "{reply address}", + "verified": "true | false" + } + }, + "sections": [ + { + "heading": "{section heading}", + "priority": "LOW | HIGH" + } + ], + "summary": "{optional one-line summary}" + } +} \ No newline at end of file diff --git a/fixtures/output-prompt-conformance/json-deep-mixed/spec.json b/fixtures/output-prompt-conformance/json-deep-mixed/spec.json new file mode 100644 index 000000000..a40e6c866 --- /dev/null +++ b/fixtures/output-prompt-conformance/json-deep-mixed/spec.json @@ -0,0 +1,27 @@ +{ + "format": "json", "rootName": "Request", "roundTrip": true, + "fields": [ + { "name": "requestId", "kind": "STRING", "required": true, "example": "req-42", "instruction": "client request id" }, + { "name": "payload", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "Payload", + "fields": [ + { "name": "title", "kind": "STRING", "required": true, "example": "Spring Sale", "instruction": "document title" }, + { "name": "author", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "Author", + "fields": [ + { "name": "name", "kind": "STRING", "required": true, "example": "Ada", "instruction": "author name" }, + { "name": "contact", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "Contact", + "fields": [ + { "name": "email", "kind": "STRING", "required": true, "example": "ada@example.com", "instruction": "reply address" }, + { "name": "verified", "kind": "BOOLEAN", "required": true, "example": "true", "instruction": "is the address verified" } + ] } } ] } }, + { "name": "sections", "kind": "OBJECT", "required": true, "array": true, "nested": { + "format": "json", "rootName": "Section", + "fields": [ + { "name": "heading", "kind": "STRING", "required": true, "example": "Intro", "instruction": "section heading" }, + { "name": "priority", "kind": "ENUM", "required": true, "enumValues": ["LOW", "HIGH"], "example": "LOW", "instruction": "render priority" } + ] } }, + { "name": "summary", "kind": "STRING", "required": false, "example": "All good", "instruction": "optional one-line summary" } + ] } } ] +} diff --git a/fixtures/output-prompt-conformance/json-depth-guard-fallback/README.md b/fixtures/output-prompt-conformance/json-depth-guard-fallback/README.md new file mode 100644 index 000000000..07da06c93 --- /dev/null +++ b/fixtures/output-prompt-conformance/json-depth-guard-fallback/README.md @@ -0,0 +1,11 @@ +# json-depth-guard-fallback + +A nine-level singular object chain (`n1 → n2 → … → n9 → leaf`) that drives the +renderer **past `MAX_NEST_DEPTH` (8)**. At depth 8 the depth guard stops recursing +and emits the flat `{n9}` placeholder instead of expanding `n9`'s `leaf` child — +proving the renderer never recurses unboundedly on a deeply (or cyclically) nested +payload. + +`roundTrip: false`: the fallback `{n9}` is a placeholder, not a real value, so the +exampleOnly fragment is intentionally not extract-clean. The point is the byte-exact +fallback shape, identical across every port (all share `MAX_NEST_DEPTH = 8`). diff --git a/fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.exampleOnly.txt b/fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.exampleOnly.txt new file mode 100644 index 000000000..e0fdfe75b --- /dev/null +++ b/fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.exampleOnly.txt @@ -0,0 +1,19 @@ +{ + "n1": { + "n2": { + "n3": { + "n4": { + "n5": { + "n6": { + "n7": { + "n8": { + "n9": "{n9}" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.guide.txt b/fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.guide.txt new file mode 100644 index 000000000..3aeb9f089 --- /dev/null +++ b/fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.guide.txt @@ -0,0 +1,31 @@ +Fill in each field as described below: +- n1 (required) +- n1.n2 (required) +- n1.n2.n3 (required) +- n1.n2.n3.n4 (required) +- n1.n2.n3.n4.n5 (required) +- n1.n2.n3.n4.n5.n6 (required) +- n1.n2.n3.n4.n5.n6.n7 (required) +- n1.n2.n3.n4.n5.n6.n7.n8 (required) +- n1.n2.n3.n4.n5.n6.n7.n8.n9 (required) + +Respond exactly like this: +{ + "n1": { + "n2": { + "n3": { + "n4": { + "n5": { + "n6": { + "n7": { + "n8": { + "n9": "{n9}" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.inline.txt b/fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.inline.txt new file mode 100644 index 000000000..e0fdfe75b --- /dev/null +++ b/fixtures/output-prompt-conformance/json-depth-guard-fallback/expected.inline.txt @@ -0,0 +1,19 @@ +{ + "n1": { + "n2": { + "n3": { + "n4": { + "n5": { + "n6": { + "n7": { + "n8": { + "n9": "{n9}" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/fixtures/output-prompt-conformance/json-depth-guard-fallback/spec.json b/fixtures/output-prompt-conformance/json-depth-guard-fallback/spec.json new file mode 100644 index 000000000..412aac5e7 --- /dev/null +++ b/fixtures/output-prompt-conformance/json-depth-guard-fallback/spec.json @@ -0,0 +1,32 @@ +{ + "format": "json", "rootName": "Root", "roundTrip": false, + "fields": [ + { "name": "n1", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "N1", "fields": [ + { "name": "n2", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "N2", "fields": [ + { "name": "n3", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "N3", "fields": [ + { "name": "n4", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "N4", "fields": [ + { "name": "n5", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "N5", "fields": [ + { "name": "n6", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "N6", "fields": [ + { "name": "n7", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "N7", "fields": [ + { "name": "n8", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "N8", "fields": [ + { "name": "n9", "kind": "OBJECT", "required": true, "nested": { + "format": "json", "rootName": "N9", "fields": [ + { "name": "leaf", "kind": "STRING", "required": true, "example": "x", "instruction": "the deepest leaf" } + ] } } + ] } } + ] } } + ] } } + ] } } + ] } } + ] } } + ] } } + ] } } ] +} diff --git a/server/csharp/MetaObjects.Render.Tests/OutputPromptConformanceTests.cs b/server/csharp/MetaObjects.Render.Tests/OutputPromptConformanceTests.cs index fc90b6e50..5b102819a 100644 --- a/server/csharp/MetaObjects.Render.Tests/OutputPromptConformanceTests.cs +++ b/server/csharp/MetaObjects.Render.Tests/OutputPromptConformanceTests.cs @@ -50,7 +50,7 @@ public void Discovers_all_output_prompt_conformance_cases() { // Count guard: a port silently skipping cases must fail CI rather than reduce coverage. // Mirrors the TS / Java / Python count guards. - Assert.Equal(12, Cases().Count()); + Assert.Equal(14, Cases().Count()); } private static readonly (string Key, PromptStyle Style)[] Styles = diff --git a/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/OutputPromptConformanceTest.kt b/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/OutputPromptConformanceTest.kt index 1fbd09db5..ca4e0f284 100644 --- a/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/OutputPromptConformanceTest.kt +++ b/server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/OutputPromptConformanceTest.kt @@ -39,7 +39,7 @@ class OutputPromptConformanceTest { private val json = ObjectMapper() /** Count guard: a port silently skipping cases must fail. Bump when adding cases. */ - private val expectedCaseCount = 12 + private val expectedCaseCount = 14 private data class StyleSpec(val key: String, val style: PromptStyle) diff --git a/server/java/render/src/test/java/com/metaobjects/render/prompt/OutputPromptConformanceTest.java b/server/java/render/src/test/java/com/metaobjects/render/prompt/OutputPromptConformanceTest.java index 65bcba2e3..ea28566d5 100644 --- a/server/java/render/src/test/java/com/metaobjects/render/prompt/OutputPromptConformanceTest.java +++ b/server/java/render/src/test/java/com/metaobjects/render/prompt/OutputPromptConformanceTest.java @@ -47,7 +47,7 @@ public class OutputPromptConformanceTest { private static final ObjectMapper JSON = new ObjectMapper(); /** Count guard: a port silently skipping cases must fail. Bump when adding cases. */ - private static final int EXPECTED_CASE_COUNT = 12; + private static final int EXPECTED_CASE_COUNT = 14; private static final Path CORPUS; static { diff --git a/server/python/tests/render/test_output_prompt_conformance.py b/server/python/tests/render/test_output_prompt_conformance.py index 03b5268e4..05a02e7a3 100644 --- a/server/python/tests/render/test_output_prompt_conformance.py +++ b/server/python/tests/render/test_output_prompt_conformance.py @@ -51,7 +51,7 @@ def _find_corpus() -> Path: _CORPUS = _find_corpus() # Count guard: a port silently skipping cases must fail. Bump when adding cases. -EXPECTED_CASE_COUNT = 12 +EXPECTED_CASE_COUNT = 14 _FORMATS = {"json": Format.JSON, "xml": Format.XML} _KINDS = { diff --git a/server/typescript/packages/render/test/output-prompt-conformance.test.ts b/server/typescript/packages/render/test/output-prompt-conformance.test.ts index 8946a477a..d3a986a2c 100644 --- a/server/typescript/packages/render/test/output-prompt-conformance.test.ts +++ b/server/typescript/packages/render/test/output-prompt-conformance.test.ts @@ -85,7 +85,7 @@ describe("output-prompt-conformance corpus", () => { .sort() : []; // Count guard: a port silently skipping cases must fail. Bump when adding cases. - const EXPECTED_CASE_COUNT = 12; + const EXPECTED_CASE_COUNT = 14; expect(names.length).toBe(EXPECTED_CASE_COUNT); for (const name of names) {