Skip to content

Commit 6b1831b

Browse files
dmealingclaude
andcommitted
test(output-prompt-conformance): Java runner reproduces corpus (10/10)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dbaf546 commit 6b1831b

1 file changed

Lines changed: 186 additions & 0 deletions

File tree

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package com.metaobjects.render.prompt;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.metaobjects.render.recover.FieldKind;
6+
import com.metaobjects.render.recover.FieldRecovery;
7+
import com.metaobjects.render.recover.FieldSpec;
8+
import com.metaobjects.render.recover.Format;
9+
import com.metaobjects.render.recover.Recover;
10+
import com.metaobjects.render.recover.RecoverOptions;
11+
import com.metaobjects.render.recover.RecoverOutcome;
12+
import com.metaobjects.render.recover.RecoverSchema;
13+
import org.junit.BeforeClass;
14+
import org.junit.Test;
15+
import org.junit.runner.RunWith;
16+
import org.junit.runners.Parameterized;
17+
18+
import java.io.IOException;
19+
import java.nio.charset.StandardCharsets;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.nio.file.Paths;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.stream.Collectors;
27+
import java.util.stream.Stream;
28+
29+
import static org.junit.Assert.assertEquals;
30+
import static org.junit.Assert.assertFalse;
31+
32+
/**
33+
* Cross-port byte-identity gate for the FR-010 output-format prompt fragment.
34+
*
35+
* <p>Reproduces every reference {@code expected.<style>.txt} in the shared corpus at
36+
* {@code fixtures/output-prompt-conformance/} using Java's {@link OutputFormatRenderer},
37+
* proving the Java renderer is byte-for-byte identical to the TS reference (and the C# port).
38+
* For {@code roundTrip} cases, also asserts the emitted example reads back cleanly through
39+
* {@link Recover} (no MALFORMED / LOST_* states).</p>
40+
*
41+
* <p>The corpus is the oracle: assertions are ordinal {@link String} equality on raw UTF-8
42+
* bytes with no newline translation.</p>
43+
*/
44+
@RunWith(Parameterized.class)
45+
public class OutputPromptConformanceTest {
46+
47+
private static final ObjectMapper JSON = new ObjectMapper();
48+
49+
/** Count guard: a port silently skipping cases must fail. Bump when adding cases. */
50+
private static final int EXPECTED_CASE_COUNT = 10;
51+
52+
private static final Path CORPUS;
53+
static {
54+
Path p = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
55+
while (p != null && !Files.exists(p.resolve("fixtures/output-prompt-conformance"))) p = p.getParent();
56+
CORPUS = p == null ? null : p.resolve("fixtures/output-prompt-conformance");
57+
}
58+
59+
private record StyleSpec(String key, PromptStyle style) {}
60+
61+
private static final List<StyleSpec> STYLES = List.of(
62+
new StyleSpec("guide", PromptStyle.GUIDE),
63+
new StyleSpec("inline", PromptStyle.INLINE),
64+
new StyleSpec("exampleOnly", PromptStyle.EXAMPLE_ONLY));
65+
66+
private final Path dir;
67+
public OutputPromptConformanceTest(String name, Path dir) { this.dir = dir; }
68+
69+
@Parameterized.Parameters(name = "{0}")
70+
public static List<Object[]> cases() throws IOException {
71+
if (CORPUS == null || !Files.isDirectory(CORPUS)) return List.of();
72+
try (Stream<Path> s = Files.list(CORPUS)) {
73+
return s.filter(Files::isDirectory)
74+
.filter(d -> Files.exists(d.resolve("spec.json")))
75+
.sorted()
76+
.map(d -> new Object[]{ d.getFileName().toString(), d })
77+
.collect(Collectors.toList());
78+
}
79+
}
80+
81+
@BeforeClass
82+
public static void countGuard() throws IOException {
83+
assertEquals("output-prompt-conformance case count", EXPECTED_CASE_COUNT, cases().size());
84+
}
85+
86+
@Test
87+
public void reproducesCorpusBytes() throws IOException {
88+
JsonNode spec = JSON.readTree(dir.resolve("spec.json").toFile());
89+
OutputFormatSpec ofs = buildOutputSpec(spec);
90+
91+
for (StyleSpec style : STYLES) {
92+
PromptOverrides overrides = new PromptOverrides(style.style(), Map.of(), Map.of());
93+
String actual = OutputFormatRenderer.render(ofs, overrides);
94+
String expected = readUtf8(dir.resolve("expected." + style.key() + ".txt"));
95+
assertEquals(dir + " style[" + style.key() + "]", expected, actual);
96+
// determinism: identical across runs
97+
assertEquals(dir + " style[" + style.key() + "] (determinism)",
98+
actual, OutputFormatRenderer.render(ofs, overrides));
99+
}
100+
101+
if (spec.has("roundTrip") && spec.get("roundTrip").asBoolean()) {
102+
String exampleFragment = readUtf8(dir.resolve("expected.exampleOnly.txt"));
103+
RecoverOutcome outcome = Recover.recover(exampleFragment, buildRecoverSchema(spec), RecoverOptions.defaults());
104+
// Skew guard: a field the renderer emitted must read back cleanly. Assert NO state
105+
// is MALFORMED or LOST_* (robust to DEFAULTED and to how a nested OBJECT-container
106+
// path classifies); any such state means the renderer emitted an example the recover
107+
// parser cannot read.
108+
for (Map.Entry<String, FieldRecovery> e : outcome.report().states().entrySet()) {
109+
FieldRecovery state = e.getValue();
110+
boolean bad = state == FieldRecovery.MALFORMED
111+
|| state == FieldRecovery.LOST_REQUIRED
112+
|| state == FieldRecovery.LOST_OPTIONAL;
113+
assertFalse(dir + " roundTrip state[" + e.getKey() + "]=" + state, bad);
114+
}
115+
}
116+
}
117+
118+
private static String readUtf8(Path p) throws IOException {
119+
return new String(Files.readAllBytes(p), StandardCharsets.UTF_8);
120+
}
121+
122+
// ----- descriptor (spec.json) -> renderer/recover model -----
123+
124+
private static OutputFormatSpec buildOutputSpec(JsonNode c) {
125+
Format fmt = toFormat(c.get("format").asText());
126+
String root = c.get("rootName").asText();
127+
List<PromptField> fields = new ArrayList<>();
128+
for (JsonNode f : c.get("fields")) fields.add(buildPromptField(f));
129+
// style is a placeholder; each render overrides it per style.
130+
return new OutputFormatSpec(fmt, root, PromptStyle.GUIDE, fields);
131+
}
132+
133+
private static PromptField buildPromptField(JsonNode f) {
134+
String name = f.get("name").asText();
135+
FieldKind kind = FieldKind.valueOf(f.get("kind").asText());
136+
boolean required = f.has("required") && f.get("required").asBoolean();
137+
boolean array = f.has("array") && f.get("array").asBoolean();
138+
List<String> enumValues = null;
139+
if (f.has("enumValues") && !f.get("enumValues").isNull()) {
140+
enumValues = new ArrayList<>();
141+
for (JsonNode v : f.get("enumValues")) enumValues.add(v.asText());
142+
}
143+
Map<String, String> enumDoc = null;
144+
if (f.has("enumDoc") && !f.get("enumDoc").isNull()) {
145+
Map<String, String> m = new java.util.LinkedHashMap<>();
146+
f.get("enumDoc").fields().forEachRemaining(e -> m.put(e.getKey(), e.getValue().asText()));
147+
enumDoc = m;
148+
}
149+
String example = f.has("example") && !f.get("example").isNull() ? f.get("example").asText() : null;
150+
String instruction = f.has("instruction") && !f.get("instruction").isNull() ? f.get("instruction").asText() : null;
151+
OutputFormatSpec nested = f.has("nested") && !f.get("nested").isNull() ? buildOutputSpec(f.get("nested")) : null;
152+
return new PromptField(name, kind, required, array, enumValues, enumDoc, example, instruction, nested);
153+
}
154+
155+
private static RecoverSchema buildRecoverSchema(JsonNode c) {
156+
Format fmt = toFormat(c.get("format").asText());
157+
String root = c.get("rootName").asText();
158+
List<FieldSpec> fields = new ArrayList<>();
159+
for (JsonNode f : c.get("fields")) fields.add(buildFieldSpec(f));
160+
return new RecoverSchema(fmt, root, fields);
161+
}
162+
163+
private static FieldSpec buildFieldSpec(JsonNode f) {
164+
String name = f.get("name").asText();
165+
FieldKind kind = FieldKind.valueOf(f.get("kind").asText());
166+
boolean required = f.has("required") && f.get("required").asBoolean();
167+
boolean array = f.has("array") && f.get("array").asBoolean();
168+
if (kind == FieldKind.ENUM) {
169+
List<String> vals = null;
170+
if (f.has("enumValues") && !f.get("enumValues").isNull()) {
171+
vals = new ArrayList<>();
172+
for (JsonNode v : f.get("enumValues")) vals.add(v.asText());
173+
}
174+
return FieldSpec.enumField(name, required, vals, Map.of());
175+
}
176+
if (kind == FieldKind.OBJECT) {
177+
RecoverSchema nested = f.has("nested") && !f.get("nested").isNull() ? buildRecoverSchema(f.get("nested")) : null;
178+
return FieldSpec.object(name, required, array, nested);
179+
}
180+
return FieldSpec.scalar(name, kind, required);
181+
}
182+
183+
private static Format toFormat(String f) {
184+
return "json".equals(f) ? Format.JSON : Format.XML;
185+
}
186+
}

0 commit comments

Comments
 (0)