fix(m9/T32b): resolve $ref in InboundAPI runtime validators (no deploy-passes/runtime-400); diamond test; ref-annotation message

This commit is contained in:
Joseph Doherty
2026-06-18 12:16:39 -04:00
parent 26e2cdef23
commit 71d5722692
9 changed files with 424 additions and 25 deletions
@@ -219,4 +219,37 @@ public class InboundApiSchemaRefTests
Assert.Null(result.Schema);
Assert.Empty(result.UnresolvedRefs);
}
// ── Diamond: two sibling refs to the SAME name resolve (not a cycle) ───────
[Fact]
public void ParseWithRefs_DiamondSiblingRefs_ResolveBothNotFalseCycle()
{
// Two sibling properties BOTH reference {"$ref":"lib:Shared"}. The shared
// name is popped from the cycle guard after each subtree is resolved, so the
// SECOND sibling must resolve too — a diamond is NOT a cycle and must not
// trip the guard. Regression for the finally-pop semantics in ResolveLibRef.
const string shared = """{"type":"object","properties":{"v":{"type":"integer"}},"required":["v"]}""";
const string json =
"""{"type":"object","properties":{"left":{"$ref":"lib:Shared"},"right":{"$ref":"lib:Shared"}}}""";
var result = InboundApiSchema.ParseWithRefs(
json,
name => name == "Shared" ? shared : null);
// Neither sibling should be reported unresolved — both resolved cleanly.
Assert.Empty(result.UnresolvedRefs);
Assert.NotNull(result.Schema);
Assert.Equal(2, result.Schema!.Fields.Count);
foreach (var sibling in new[] { "left", "right" })
{
var field = Assert.Single(result.Schema.Fields, f => f.Name == sibling);
Assert.Equal("object", field.Schema.Type);
var inner = Assert.Single(field.Schema.Fields);
Assert.Equal("v", inner.Name);
Assert.True(inner.Required);
Assert.Equal("integer", inner.Schema.Type);
}
}
}