Skip to content

Commit 74bdafe

Browse files
dmealingclaude
andcommitted
test(csharp): source-v2 un-gates conformance + adds SourceV2Tests
Empties both conformance ledgers (JSON: 24 entries → 0; YAML: 1 entry → 0). With source.rdb + @kind/@role/@table/@Schema registered, one-primary multi-source validation wired, relationship @onDelete/@onUpdate enforced, @column registered as a common-field attr, and ERR_RESERVED_ATTR firing in the canonical parser, every previously gated v2 fixture passes natively. ERR_RESERVED_ATTR now pushes to the structured error list (not warnings) in lax mode, matching the TS parser-core.ts pattern (lax → errors-sink-direct, strict → throw). Without this, ERR_RESERVED_ATTR was silently a warning and the error-reserved-word-as-attr conformance check saw zero errors. Updates Core_provider_registers_exactly_N_types from 70 → 69 to reflect source's collapse from 3 subtypes (base, dbTable, dbView) to 2 (base, rdb). SourceV2Tests covers the Stage 2 Unit 1 surface with named tests: - source.rdb registration + bad-value rejection on @kind / @ROLE - MetaSource accessor defaults + read-only-kind derivation - One-primary rule (0 / 1 / 0 primary / 2+ primary) - Relationship explicit + default @onDelete / @onUpdate + bad-value rejection - ERR_RESERVED_ATTR on @-prefixed reserved words, with bare form accepted Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8da7f2d commit 74bdafe

5 files changed

Lines changed: 363 additions & 40 deletions

File tree

server/csharp/MetaObjects.Conformance.Tests/CoreTypesTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ public void Core_provider_factory_builds_the_right_node_class()
2828
}
2929

3030
[Fact]
31-
public void Core_provider_registers_exactly_70_types()
31+
public void Core_provider_registers_exactly_69_types()
3232
{
3333
// Guard: if a subtype array gains an entry without a matching
3434
// FieldDataType / AttrDataType mapping, RegisterCoreTypeDefs throws and
3535
// this test catches the mismatch before it reaches downstream callers.
36-
// 70 = 64 + template.{base,prompt,output} + origin.collection + identity.reference (FR-003/004) + field.enum.
36+
// 69 = was 70 (source had 3 subtypes: base, dbTable, dbView)
37+
// - source-v2 (ADR-0007) collapses to 2 subtypes (base, rdb).
3738
var reg = Provider.ComposeRegistry(new[] { CoreTypes.CoreTypesProvider });
38-
Assert.Equal(70, reg.AllTypes().Count);
39+
Assert.Equal(69, reg.AllTypes().Count);
3940
}
4041
}
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
// SourceV2Tests — targeted tests for the source-v2 (ADR-0007) port: source.rdb
2+
// registration + accessors + bad-value rejection, one-primary multi-source
3+
// validation, relationship referential actions, and ERR_RESERVED_ATTR
4+
// enforcement in the canonical parser.
5+
//
6+
// Mirrors the Java Stage 2 Unit 1 test layout. The corresponding fixtures live
7+
// in fixtures/conformance/source-* and error-* (used by ConformanceTests too);
8+
// these tests give each behavior a named entry point with a clear failure
9+
// message and a small inline JSON variant where round-trip + accessor checks
10+
// are convenient.
11+
12+
using MetaObjects;
13+
using MetaObjects.Loader;
14+
using MetaObjects.Meta;
15+
using Xunit;
16+
17+
namespace MetaObjects.Conformance.Tests;
18+
19+
public class SourceV2Tests
20+
{
21+
private static LoadResult LoadInline(string json) =>
22+
new MetaDataLoader().Load([new InMemorySource(json, id: "inline.json")]);
23+
24+
// -------------------------------------------------------------------------
25+
// source.rdb registration + accessors
26+
// -------------------------------------------------------------------------
27+
28+
[Fact]
29+
public void Source_rdb_is_registered_with_table_kind_role_schema_attrs()
30+
{
31+
var reg = Provider.ComposeRegistry(new[] { CoreTypes.CoreTypesProvider });
32+
Assert.True(reg.Has("source", "rdb"));
33+
34+
var def = reg.Find("source", "rdb")!;
35+
var attrNames = def.Attributes.Select(a => a.Name).ToHashSet();
36+
Assert.Contains("table", attrNames);
37+
Assert.Contains("kind", attrNames);
38+
Assert.Contains("role", attrNames);
39+
Assert.Contains("schema", attrNames);
40+
}
41+
42+
[Fact]
43+
public void Source_rdb_kind_enum_rejects_unknown_value()
44+
{
45+
const string json = """
46+
{ "metadata.root": { "package": "acme", "children": [
47+
{ "object.entity": { "name": "Product", "children": [
48+
{ "source.rdb": { "@table": "products", "@kind": "nosuchkind" } },
49+
{ "field.long": { "name": "id" } },
50+
{ "identity.primary": { "@fields": "id" } }
51+
]}}
52+
]}}
53+
""";
54+
var result = LoadInline(json);
55+
Assert.Contains(result.Errors, e => e.Code == ErrorCode.ERR_BAD_ATTR_VALUE);
56+
}
57+
58+
[Fact]
59+
public void Source_rdb_role_enum_rejects_unknown_value()
60+
{
61+
const string json = """
62+
{ "metadata.root": { "package": "acme", "children": [
63+
{ "object.entity": { "name": "Product", "children": [
64+
{ "source.rdb": { "@table": "products", "@role": "shadow" } },
65+
{ "field.long": { "name": "id" } },
66+
{ "identity.primary": { "@fields": "id" } }
67+
]}}
68+
]}}
69+
""";
70+
var result = LoadInline(json);
71+
Assert.Contains(result.Errors, e => e.Code == ErrorCode.ERR_BAD_ATTR_VALUE);
72+
}
73+
74+
[Fact]
75+
public void MetaSource_accessors_default_when_attrs_absent()
76+
{
77+
const string json = """
78+
{ "metadata.root": { "package": "acme", "children": [
79+
{ "object.entity": { "name": "Product", "children": [
80+
{ "source.rdb": { "@table": "products" } },
81+
{ "field.long": { "name": "id" } },
82+
{ "identity.primary": { "@fields": "id" } }
83+
]}}
84+
]}}
85+
""";
86+
var result = LoadInline(json);
87+
Assert.Empty(result.Errors);
88+
89+
var obj = (MetaObject)result.Root.Children().First(c => c.Type == "object");
90+
var src = obj.OwnSources().Single();
91+
Assert.Equal("products", src.TableName);
92+
Assert.Equal("table", src.EffectiveKind);
93+
Assert.Equal("primary", src.Role);
94+
Assert.True(src.IsWritable());
95+
Assert.False(src.IsReadOnly());
96+
}
97+
98+
[Fact]
99+
public void MetaSource_read_only_kind_makes_source_read_only()
100+
{
101+
const string json = """
102+
{ "metadata.root": { "package": "acme", "children": [
103+
{ "object.entity": { "name": "ProductView", "children": [
104+
{ "source.rdb": { "@table": "v_products", "@kind": "view" } },
105+
{ "field.long": { "name": "id" } },
106+
{ "identity.primary": { "@fields": "id" } }
107+
]}}
108+
]}}
109+
""";
110+
var result = LoadInline(json);
111+
Assert.Empty(result.Errors);
112+
113+
var obj = (MetaObject)result.Root.Children().First(c => c.Type == "object");
114+
var src = obj.OwnSources().Single();
115+
Assert.Equal("view", src.EffectiveKind);
116+
Assert.True(src.IsReadOnly());
117+
Assert.False(src.IsWritable());
118+
}
119+
120+
// -------------------------------------------------------------------------
121+
// One-primary multi-source validation
122+
// -------------------------------------------------------------------------
123+
124+
[Fact]
125+
public void Zero_sources_is_OK()
126+
{
127+
const string json = """
128+
{ "metadata.root": { "package": "acme", "children": [
129+
{ "object.value": { "name": "Address", "children": [
130+
{ "field.string": { "name": "city" } }
131+
]}}
132+
]}}
133+
""";
134+
Assert.Empty(LoadInline(json).Errors);
135+
}
136+
137+
[Fact]
138+
public void Single_primary_source_is_OK()
139+
{
140+
const string json = """
141+
{ "metadata.root": { "package": "acme", "children": [
142+
{ "object.entity": { "name": "Product", "children": [
143+
{ "source.rdb": { "@table": "products" } },
144+
{ "field.long": { "name": "id" } },
145+
{ "identity.primary": { "@fields": "id" } }
146+
]}}
147+
]}}
148+
""";
149+
Assert.Empty(LoadInline(json).Errors);
150+
}
151+
152+
[Fact]
153+
public void No_primary_among_sources_is_ERR_SOURCE_NO_PRIMARY()
154+
{
155+
const string json = """
156+
{ "metadata.root": { "package": "acme", "children": [
157+
{ "object.entity": { "name": "Product", "children": [
158+
{ "source.rdb": { "@role": "replica", "@table": "products_r" } },
159+
{ "field.long": { "name": "id" } },
160+
{ "identity.primary": { "@fields": "id" } }
161+
]}}
162+
]}}
163+
""";
164+
var result = LoadInline(json);
165+
Assert.Contains(result.Errors, e => e.Code == ErrorCode.ERR_SOURCE_NO_PRIMARY);
166+
}
167+
168+
[Fact]
169+
public void Multiple_primary_sources_is_ERR_SOURCE_MULTIPLE_PRIMARY()
170+
{
171+
const string json = """
172+
{ "metadata.root": { "package": "acme", "children": [
173+
{ "object.entity": { "name": "Product", "children": [
174+
{ "source.rdb": { "@table": "products_a" } },
175+
{ "source.rdb": { "@table": "products_b" } },
176+
{ "field.long": { "name": "id" } },
177+
{ "identity.primary": { "@fields": "id" } }
178+
]}}
179+
]}}
180+
""";
181+
var result = LoadInline(json);
182+
Assert.Contains(result.Errors, e => e.Code == ErrorCode.ERR_SOURCE_MULTIPLE_PRIMARY);
183+
}
184+
185+
// -------------------------------------------------------------------------
186+
// Relationship @onDelete / @onUpdate referential actions
187+
// -------------------------------------------------------------------------
188+
189+
[Fact]
190+
public void Relationship_explicit_onDelete_round_trips_and_resolves()
191+
{
192+
const string json = """
193+
{ "metadata.root": { "package": "acme", "children": [
194+
{ "object.entity": { "name": "Program", "children": [
195+
{ "source.rdb": { "@table": "programs" } },
196+
{ "field.long": { "name": "id" } },
197+
{ "identity.primary": { "@fields": "id" } }
198+
]}},
199+
{ "object.entity": { "name": "Week", "children": [
200+
{ "source.rdb": { "@table": "weeks" } },
201+
{ "field.long": { "name": "id" } },
202+
{ "field.long": { "name": "programId" } },
203+
{ "relationship.composition": { "name": "program",
204+
"@objectRef": "Program", "@cardinality": "one",
205+
"@onDelete": "cascade", "@onUpdate": "cascade" } },
206+
{ "identity.primary": { "@fields": "id" } },
207+
{ "identity.reference": { "name": "programRef",
208+
"@fields": "programId", "@references": "Program" } }
209+
]}}
210+
]}}
211+
""";
212+
var result = LoadInline(json);
213+
Assert.Empty(result.Errors);
214+
215+
var week = (MetaObject)result.Root.Children().First(o => o.Name == "Week");
216+
var rel = week.Relationships().Single();
217+
Assert.Equal("cascade", rel.EffectiveOnDelete);
218+
Assert.Equal("cascade", rel.EffectiveOnUpdate);
219+
}
220+
221+
[Fact]
222+
public void Relationship_subtype_default_resolves_when_attr_absent()
223+
{
224+
const string json = """
225+
{ "metadata.root": { "package": "acme", "children": [
226+
{ "object.entity": { "name": "Program", "children": [
227+
{ "source.rdb": { "@table": "programs" } },
228+
{ "field.long": { "name": "id" } },
229+
{ "identity.primary": { "@fields": "id" } }
230+
]}},
231+
{ "object.entity": { "name": "Week", "children": [
232+
{ "source.rdb": { "@table": "weeks" } },
233+
{ "field.long": { "name": "id" } },
234+
{ "field.long": { "name": "programId" } },
235+
{ "relationship.composition": { "name": "program",
236+
"@objectRef": "Program", "@cardinality": "one" } },
237+
{ "identity.primary": { "@fields": "id" } },
238+
{ "identity.reference": { "name": "programRef",
239+
"@fields": "programId", "@references": "Program" } }
240+
]}}
241+
]}}
242+
""";
243+
var result = LoadInline(json);
244+
Assert.Empty(result.Errors);
245+
246+
var week = (MetaObject)result.Root.Children().First(o => o.Name == "Week");
247+
var rel = week.Relationships().Single();
248+
// composition default → cascade; update default → cascade.
249+
Assert.Equal("cascade", rel.EffectiveOnDelete);
250+
Assert.Equal("cascade", rel.EffectiveOnUpdate);
251+
}
252+
253+
[Fact]
254+
public void Relationship_bad_onDelete_value_is_ERR_BAD_ATTR_VALUE()
255+
{
256+
const string json = """
257+
{ "metadata.root": { "package": "acme", "children": [
258+
{ "object.entity": { "name": "Program", "children": [
259+
{ "source.rdb": { "@table": "programs" } },
260+
{ "field.long": { "name": "id" } },
261+
{ "identity.primary": { "@fields": "id" } }
262+
]}},
263+
{ "object.entity": { "name": "Week", "children": [
264+
{ "source.rdb": { "@table": "weeks" } },
265+
{ "field.long": { "name": "id" } },
266+
{ "field.long": { "name": "programId" } },
267+
{ "relationship.composition": { "name": "program",
268+
"@objectRef": "Program", "@cardinality": "one",
269+
"@onDelete": "destroyAll" } },
270+
{ "identity.primary": { "@fields": "id" } },
271+
{ "identity.reference": { "name": "programRef",
272+
"@fields": "programId", "@references": "Program" } }
273+
]}}
274+
]}}
275+
""";
276+
var result = LoadInline(json);
277+
Assert.Contains(result.Errors, e => e.Code == ErrorCode.ERR_BAD_ATTR_VALUE);
278+
}
279+
280+
// -------------------------------------------------------------------------
281+
// ERR_RESERVED_ATTR enforcement in the canonical parser
282+
// -------------------------------------------------------------------------
283+
284+
[Fact]
285+
public void At_prefixed_isArray_is_ERR_RESERVED_ATTR()
286+
{
287+
const string json = """
288+
{ "metadata.root": { "package": "acme", "children": [
289+
{ "object.entity": { "name": "Owner", "children": [
290+
{ "field.long": { "name": "id" } },
291+
{ "field.object": { "name": "items", "@isArray": true, "@objectRef": "Item" } },
292+
{ "identity.primary": { "name": "pk", "@fields": "id" } }
293+
]}},
294+
{ "object.entity": { "name": "Item", "children": [
295+
{ "field.long": { "name": "id" } },
296+
{ "identity.primary": { "name": "pk", "@fields": "id" } }
297+
]}}
298+
]}}
299+
""";
300+
var result = LoadInline(json);
301+
Assert.Contains(result.Errors, e => e.Code == ErrorCode.ERR_RESERVED_ATTR);
302+
}
303+
304+
[Fact]
305+
public void At_prefixed_name_is_ERR_RESERVED_ATTR()
306+
{
307+
const string json = """
308+
{ "metadata.root": { "package": "acme", "children": [
309+
{ "object.entity": { "name": "Owner", "children": [
310+
{ "field.long": { "@name": "id" } },
311+
{ "identity.primary": { "name": "pk", "@fields": "id" } }
312+
]}}
313+
]}}
314+
""";
315+
var result = LoadInline(json);
316+
Assert.Contains(result.Errors, e => e.Code == ErrorCode.ERR_RESERVED_ATTR);
317+
}
318+
319+
[Fact]
320+
public void Bare_isArray_structural_key_is_OK()
321+
{
322+
const string json = """
323+
{ "metadata.root": { "package": "acme", "children": [
324+
{ "object.entity": { "name": "Owner", "children": [
325+
{ "field.long": { "name": "id" } },
326+
{ "field.object": { "name": "items", "isArray": true, "@objectRef": "Item" } },
327+
{ "identity.primary": { "name": "pk", "@fields": "id" } }
328+
]}},
329+
{ "object.entity": { "name": "Item", "children": [
330+
{ "field.long": { "name": "id" } },
331+
{ "identity.primary": { "name": "pk", "@fields": "id" } }
332+
]}}
333+
]}}
334+
""";
335+
var result = LoadInline(json);
336+
Assert.Empty(result.Errors);
337+
}
338+
}
Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,5 @@
11
{
22
"language": "csharp",
3-
"fixtures": [
4-
"doc-common-attrs-on-all-types",
5-
"error-field-object-storage-flattened-array",
6-
"error-field-object-storage-no-object-ref",
7-
"error-origin-bad-aggregate-fn",
8-
"error-origin-bad-via-path",
9-
"error-reserved-word-as-attr",
10-
"error-source-multiple-primary",
11-
"error-source-no-primary",
12-
"field-object-storage-flattened",
13-
"field-object-storage-flattened-nullable",
14-
"field-object-storage-jsonb-array",
15-
"field-object-storage-jsonb-single",
16-
"origin-aggregate-count",
17-
"origin-aggregate-sum",
18-
"origin-multi-level-via",
19-
"origin-passthrough-simple",
20-
"source-db-table-default-schema-omitted",
21-
"source-db-table-explicit",
22-
"source-db-table-with-schema",
23-
"source-db-view-projection",
24-
"source-db-view-with-schema",
25-
"source-multi-source-roles",
26-
"source-rdb-column",
27-
"source-rdb-referential-actions"
28-
]
3+
"_comment": "Source-v2 (ADR-0007) un-gated the v1 source fixtures + the multi-primary + reserved-attr fixtures + the field-object-storage cluster + origin cluster + doc-common-attrs-on-all-types. All conformance fixtures pass.",
4+
"fixtures": []
295
}
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
{
22
"language": "csharp",
3-
"_comment": "C# v1 metadata vocabulary doesn't declare @column on field.string, so the YAML D2 coercion guard has no schema to compare `column: TRUE` against — no ERR_YAML_COERCION fires. The 3 v2-vocabulary happy-paths and the num-in-enum fixture (which uses v1-declared @values on field.enum) pass natively.",
4-
"fixtures": [
5-
"error-yaml-coerced-bool-in-string"
6-
]
3+
"_comment": "Source-v2 (ADR-0007) registered @column on every field subtype, so the YAML D2 coercion guard now catches `column: TRUE`. All YAML conformance fixtures pass.",
4+
"fixtures": []
75
}

0 commit comments

Comments
 (0)