Skip to content

Commit d8ac40b

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

1 file changed

Lines changed: 203 additions & 0 deletions

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""Cross-language output-prompt-conformance corpus runner — FR-010 prompt gate.
2+
3+
Each case dir under ``fixtures/output-prompt-conformance/`` holds:
4+
5+
- ``spec.json`` the output-format descriptor (format / rootName /
6+
roundTrip? / fields[{name, kind, required, array?, example?, instruction?,
7+
enumValues?, enumDoc?, nested?}]).
8+
- ``expected.guide.txt`` / ``expected.inline.txt`` / ``expected.exampleOnly.txt``
9+
byte-exact reference text (TS-produced; C# + Java reproduce it byte-for-byte).
10+
11+
The runner reproduces those bytes EXACTLY using Python's ``render_output_format``,
12+
proving cross-port byte-identity. The corpus is the oracle — do not weaken
13+
assertions or edit the fixtures. 1:1 port of ``output-prompt-conformance.test.ts``.
14+
"""
15+
from __future__ import annotations
16+
17+
import json
18+
from pathlib import Path
19+
20+
import pytest
21+
22+
from metaobjects.render import (
23+
OutputFormatSpec,
24+
PromptField,
25+
PromptOverrides,
26+
PromptStyle,
27+
render_output_format,
28+
)
29+
from metaobjects.render.recover import (
30+
FieldKind,
31+
FieldRecovery,
32+
FieldSpec,
33+
Format,
34+
RecoverSchema,
35+
recover,
36+
)
37+
38+
39+
def _find_corpus() -> Path:
40+
p = Path(__file__).resolve()
41+
while p != p.parent:
42+
candidate = p / "fixtures" / "output-prompt-conformance"
43+
if candidate.is_dir():
44+
return candidate
45+
p = p.parent
46+
raise RuntimeError(
47+
"fixtures/output-prompt-conformance not found walking up from tests/"
48+
)
49+
50+
51+
_CORPUS = _find_corpus()
52+
53+
# Count guard: a port silently skipping cases must fail. Bump when adding cases.
54+
EXPECTED_CASE_COUNT = 10
55+
56+
_FORMATS = {"json": Format.JSON, "xml": Format.XML}
57+
_KINDS = {
58+
"STRING": FieldKind.STRING,
59+
"INT": FieldKind.INT,
60+
"LONG": FieldKind.LONG,
61+
"DOUBLE": FieldKind.DOUBLE,
62+
"BOOLEAN": FieldKind.BOOLEAN,
63+
"ENUM": FieldKind.ENUM,
64+
"OBJECT": FieldKind.OBJECT,
65+
}
66+
67+
_STYLES: list[tuple[str, PromptStyle]] = [
68+
("guide", PromptStyle.GUIDE),
69+
("inline", PromptStyle.INLINE),
70+
("exampleOnly", PromptStyle.EXAMPLE_ONLY),
71+
]
72+
73+
74+
def _cases() -> list[str]:
75+
return sorted(
76+
d.name for d in _CORPUS.iterdir() if (d / "spec.json").is_file()
77+
)
78+
79+
80+
def _str_or_none(v: object) -> str | None:
81+
return None if v is None else str(v)
82+
83+
84+
def _build_output_spec(c: dict[str, object]) -> OutputFormatSpec:
85+
fmt = _FORMATS[str(c["format"])]
86+
root_name = str(c["rootName"])
87+
fields_raw = c["fields"]
88+
assert isinstance(fields_raw, list)
89+
fields: list[PromptField] = []
90+
for f in fields_raw:
91+
assert isinstance(f, dict)
92+
enum_values_raw = f.get("enumValues")
93+
enum_values = (
94+
[str(v) for v in enum_values_raw]
95+
if isinstance(enum_values_raw, list)
96+
else None
97+
)
98+
enum_doc_raw = f.get("enumDoc")
99+
enum_doc = (
100+
{str(k): str(v) for k, v in enum_doc_raw.items()}
101+
if isinstance(enum_doc_raw, dict)
102+
else None
103+
)
104+
nested_raw = f.get("nested")
105+
nested = (
106+
_build_output_spec(nested_raw) if isinstance(nested_raw, dict) else None
107+
)
108+
fields.append(
109+
PromptField(
110+
name=str(f["name"]),
111+
kind=_KINDS[str(f["kind"])],
112+
required=bool(f["required"]),
113+
array=bool(f.get("array", False)),
114+
enum_values=enum_values,
115+
enum_doc=enum_doc,
116+
example=_str_or_none(f.get("example")),
117+
instruction=_str_or_none(f.get("instruction")),
118+
nested=nested,
119+
)
120+
)
121+
# style here is a placeholder; each render overrides it per style.
122+
return OutputFormatSpec(
123+
format=fmt, root_name=root_name, style=PromptStyle.GUIDE, fields=fields
124+
)
125+
126+
127+
def _build_recover_schema(c: dict[str, object]) -> RecoverSchema:
128+
fmt = _FORMATS[str(c["format"])]
129+
root_name = str(c["rootName"])
130+
fields_raw = c["fields"]
131+
assert isinstance(fields_raw, list)
132+
fields: list[FieldSpec] = []
133+
for f in fields_raw:
134+
assert isinstance(f, dict)
135+
name = str(f["name"])
136+
kind_token = str(f["kind"])
137+
required = bool(f["required"])
138+
if kind_token == "ENUM":
139+
enum_values_raw = f.get("enumValues")
140+
enum_values = (
141+
[str(v) for v in enum_values_raw]
142+
if isinstance(enum_values_raw, list)
143+
else None
144+
)
145+
fields.append(FieldSpec.enum_field(name, required, enum_values, {}))
146+
elif kind_token == "OBJECT":
147+
nested_raw = f.get("nested")
148+
nested = (
149+
_build_recover_schema(nested_raw)
150+
if isinstance(nested_raw, dict)
151+
else None
152+
)
153+
fields.append(
154+
FieldSpec.object_(name, required, bool(f.get("array", False)), nested)
155+
)
156+
else:
157+
fields.append(FieldSpec.scalar(name, _KINDS[kind_token], required))
158+
return RecoverSchema(fmt, root_name, fields)
159+
160+
161+
def _read_text(path: Path) -> str:
162+
# Byte-exact: decode bytes directly so no newline translation occurs.
163+
return path.read_bytes().decode("utf-8")
164+
165+
166+
def test_discovers_all_output_prompt_conformance_cases() -> None:
167+
"""Count guard mirrors the TS / C# / Java runners — a deleted/added fixture must
168+
fail CI rather than silently change coverage."""
169+
assert len(_cases()) == EXPECTED_CASE_COUNT
170+
171+
172+
@pytest.mark.parametrize("case_name", _cases())
173+
def test_renders_corpus_byte_exact(case_name: str) -> None:
174+
case_dir = _CORPUS / case_name
175+
spec_dict = json.loads((case_dir / "spec.json").read_text())
176+
ofs = _build_output_spec(spec_dict)
177+
178+
for key, style in _STYLES:
179+
overrides = PromptOverrides(style=style)
180+
actual = render_output_format(ofs, overrides)
181+
expected = _read_text(case_dir / f"expected.{key}.txt")
182+
assert actual == expected, f"{case_name}/{key}: byte drift"
183+
# determinism: identical across runs
184+
assert render_output_format(ofs, overrides) == actual, (
185+
f"{case_name}/{key}: non-deterministic render"
186+
)
187+
188+
if spec_dict.get("roundTrip"):
189+
example_fragment = _read_text(case_dir / "expected.exampleOnly.txt")
190+
outcome = recover(example_fragment, _build_recover_schema(spec_dict))
191+
# Skew guard: a field the renderer emitted must read back cleanly. NO state
192+
# may be MALFORMED or LOST_* (robust to DEFAULTED and to how a nested
193+
# OBJECT-container path classifies); any such state means the renderer
194+
# emitted an example the recover parser cannot read.
195+
bad = {
196+
FieldRecovery.MALFORMED,
197+
FieldRecovery.LOST_REQUIRED,
198+
FieldRecovery.LOST_OPTIONAL,
199+
}
200+
for field_path, state in outcome.report.states().items():
201+
assert state not in bad, (
202+
f"{case_name}: field {field_path!r} round-tripped as {state}"
203+
)

0 commit comments

Comments
 (0)