Skip to content

Commit 9414ef9

Browse files
dmealingclaude
andcommitted
test(gate): RED tripwires for inherit-without-restate codegen bug (#56)
Two failing gates (RED until #56 is fixed) asserting codegen reads INHERITED attrs via the effective accessor, not own-only: - TS (codegen-ts): an identity.primary that inherits @fields via node-level extends without restating it must still yield a primary key. pk-resolver reads primary.ownAttr(@fields) → undefined → the entity is dropped from the PK map → no PK. - Python (codegen): a field that inherits @required via extends (abstract-field reuse) must generate as a REQUIRED Pydantic field, not Optional. entity_model reads field.attr(@required) — which in this port is OWN-ONLY despite the name — so the inherited @required is invisible and `label: str | None = None` is wrongly emitted. Both assert the CORRECT behavior and turn green when the ownAttr→attr fix lands (see #56 for the full cross-port inventory). Intentional RED tripwire PR — do not merge until #56 is fixed; then these become the permanent regression gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0e8af0c commit 9414ef9

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""GATE / TRIPWIRE for issue #56 — codegen must read inherited attributes via the
2+
EFFECTIVE accessor (``attrs()``), not the own-only ``attr()`` (which, despite its
3+
name, does NOT walk the ``extends`` super chain in this port).
4+
5+
This test asserts the CORRECT behavior and is RED until #56 is fixed: a field that
6+
inherits ``@required`` via ``extends`` (abstract-field reuse) without restating it must
7+
generate as a REQUIRED Pydantic field, not ``Optional``. ``entity_model.py`` currently
8+
reads ``field.attr(@required)`` (own-only) → the inherited ``@required`` is invisible →
9+
the field is wrongly emitted as optional. See #56 for the full cross-port inventory.
10+
"""
11+
from __future__ import annotations
12+
13+
from metaobjects import MetaDataLoader
14+
from metaobjects.meta.core.object.meta_object import MetaObject
15+
from metaobjects.codegen.generators.entity_model import render_entity_model
16+
17+
_MODEL = """
18+
{ "metadata.root": { "package": "test", "children": [
19+
{ "object.entity": { "name": "Base", "abstract": true, "children": [
20+
{ "field.string": { "name": "baseLabel", "@required": true, "@maxLength": 80 } }
21+
] } },
22+
{ "object.entity": { "name": "Widget", "children": [
23+
{ "source.rdb": { "@table": "widgets" } },
24+
{ "field.long": { "name": "id" } },
25+
{ "field.string": { "name": "label", "extends": "test::Base.baseLabel" } },
26+
{ "identity.primary": { "name": "primary", "@fields": ["id"], "@generation": "increment" } }
27+
] } }
28+
] } }
29+
"""
30+
31+
32+
def _widget() -> MetaObject:
33+
result = MetaDataLoader.from_string(_MODEL)
34+
assert not result.errors, [str(e) for e in result.errors]
35+
widget = next(c for c in result.root.children()
36+
if getattr(c, "name", None) == "Widget")
37+
assert isinstance(widget, MetaObject)
38+
return widget
39+
40+
41+
def test_inherited_required_field_generates_as_required() -> None:
42+
widget = _widget()
43+
label = next(f for f in widget.fields() if f.name == "label")
44+
45+
# Root cause: effective sees the inherited @required; own-only does not.
46+
assert label.attrs().get("required") is True # effective
47+
assert label.attr("required") is None # own-only misses it (the trap)
48+
49+
# The gate: an inherited @required must produce a REQUIRED field, not Optional.
50+
# RED until #56 is fixed (entity_model reads field.attr(@required) own-only).
51+
source = render_entity_model(widget)
52+
assert "label: str" in source, source
53+
assert "label: Optional" not in source, source
54+
assert "label: str | None" not in source, source
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// GATE / TRIPWIRE for issue #56 — codegen must read inherited attributes via the
2+
// EFFECTIVE accessor (attr / attrs), not the OWN-ONLY accessor (ownAttr).
3+
//
4+
// These tests assert the CORRECT behavior and are RED until #56 is fixed. When the
5+
// ownAttr→attr fix lands, they go green and this becomes the permanent regression gate.
6+
// See issue #56 for the full cross-port inventory of own-only reads.
7+
8+
import { describe, test, expect } from "bun:test";
9+
import { MetaDataLoader, InMemoryStringSource } from "@metaobjectsdev/metadata";
10+
import { buildPkMap } from "../src/pk-resolver.js";
11+
12+
async function loadModel(children: unknown[]) {
13+
const json = JSON.stringify({ "metadata.root": { package: "test", children } });
14+
const result = await new MetaDataLoader().load([new InMemoryStringSource(json)]);
15+
expect(result.errors).toEqual([]);
16+
return result.root;
17+
}
18+
19+
describe("inherit-without-restate gate (#56)", () => {
20+
// An identity.primary that inherits @fields via node-level `extends` (ADR-0029) without
21+
// restating it. pk-resolver.ts reads primary.ownAttr(@fields) → undefined → the entity
22+
// is silently dropped from the PK map → generated schema has NO primary key.
23+
test("identity.primary extends a base identity (no @fields restated) still yields a PK", async () => {
24+
const root = await loadModel([
25+
{
26+
"object.entity": {
27+
name: "BaseEntity",
28+
abstract: true,
29+
children: [
30+
{ "field.long": { name: "id" } },
31+
{ "identity.primary": { name: "primary", "@fields": ["id"], "@generation": "increment" } },
32+
],
33+
},
34+
},
35+
{
36+
"object.entity": {
37+
name: "Widget",
38+
children: [
39+
{ "source.rdb": { "@table": "widgets" } },
40+
{ "field.long": { name: "id" } },
41+
{ "identity.primary": { name: "primary", extends: "test::BaseEntity.primary" } },
42+
],
43+
},
44+
},
45+
]);
46+
47+
const widget = root.objects().find((o) => o.name === "Widget")!;
48+
const primary = widget.ownChildren().find((c) => c.type === "identity")!;
49+
50+
// Root cause documented: effective sees the inherited value; own-only does not.
51+
expect(primary.attr("fields")).toEqual(["id"]);
52+
expect(primary.ownAttr("fields")).toBeUndefined();
53+
54+
// The gate: the entity must have a primary key. RED until #56 is fixed
55+
// (pk-resolver currently drops Widget because it reads ownAttr(@fields)).
56+
const pkMap = buildPkMap(root);
57+
expect([...pkMap.keys()]).toContain("Widget");
58+
});
59+
});

0 commit comments

Comments
 (0)