@@ -715,6 +715,101 @@ func TestConst_BecomesLiteral(t *testing.T) {
715715 assert .Equal (t , ir.Value {Kind : ir .ValueString , Str : "fixed" }, k .Value )
716716}
717717
718+ func TestHoistLiteral_UnconvertibleConstBecomesAny (t * testing.T ) {
719+ t .Parallel ()
720+ // A custom tag is structurally unconvertible (no scalarValue case resolves
721+ // it), forcing hoistLiteral's fallback. Before the fix this silently
722+ // produced a Literal asserting the value is null, which the spec never said.
723+ spec := componentSpec (" K:\n const: !foo bar\n " )
724+ doc , diags := lowerSpec (t , spec )
725+ k , ok := doc .Types [componentID ("K" )].(* ir.Any )
726+ require .True (t , ok , "an unconvertible const hoists the schemaless top type at its own pointer" )
727+ assert .Equal (t , ir .KindAny , k .Kind ())
728+ for _ , td := range doc .Types {
729+ _ , isLiteral := td .(* ir.Literal )
730+ assert .False (t , isLiteral , "no Literal is produced anywhere; nothing lies about the value being null" )
731+ }
732+ require .Equal (t , 1 , countDiagsAt (diags , codeDegradedConstruct , ir .SeverityWarning ),
733+ "exactly one warning fires for the unconvertible value" )
734+ d , ok := firstDegradedWarning (diags )
735+ require .True (t , ok )
736+ assert .Equal (t , "/components/schemas/K" , d .Provenance .Pointer )
737+ }
738+
739+ func TestEnumAsUnion_UnconvertibleMemberBecomesAny (t * testing.T ) {
740+ t .Parallel ()
741+ // The convertible member ("ok") must still hoist a real Literal; only the
742+ // genuinely unconvertible member ("!foo bar") falls back to the top type.
743+ spec := componentSpec (" M:\n enum: [ok, !foo bar]\n " )
744+ doc , diags := lowerSpec (t , spec )
745+ u , ok := doc .Types [componentID ("M" )].(* ir.Union )
746+ require .True (t , ok , "heterogeneous enum still lowers to a union of literals" )
747+ require .Len (t , u .Variants , 2 )
748+
749+ member0 , ok := doc .Types [u .Variants [0 ].Type .Target ].(* ir.Literal )
750+ require .True (t , ok , "the convertible member still hoists a Literal" )
751+ assert .Equal (t , ir.Value {Kind : ir .ValueString , Str : "ok" }, member0 .Value )
752+
753+ member1 , ok := doc .Types [u .Variants [1 ].Type .Target ].(* ir.Any )
754+ require .True (t , ok , "the unconvertible member hoists the schemaless top type, not a lying null Literal" )
755+ assert .Equal (t , ir .KindAny , member1 .Kind ())
756+
757+ require .Equal (t , 1 , countDiagsAt (diags , codeDegradedConstruct , ir .SeverityWarning ),
758+ "exactly one warning for the unconvertible member, distinct from the heterogeneous-enum info diagnostic" )
759+ d , ok := firstDegradedWarning (diags )
760+ require .True (t , ok )
761+ assert .Equal (t , "/components/schemas/M/enum/1" , d .Provenance .Pointer )
762+ }
763+
764+ func TestEnum_UnquotedDatesStayClosedEnum (t * testing.T ) {
765+ t .Parallel ()
766+ // The exact issue repro: YAML 1.1 resolves an unquoted date to !!timestamp.
767+ // Before the fix, scalarValue had no case for it, so both enum members
768+ // failed to convert, enumMembers bailed to enumAsUnion, and valueOrNull
769+ // turned every member into a null literal — the actual dates never
770+ // survived. A component-level default (as in the repro) is never lowered
771+ // onto anything by itself (only properties/params read one), so it
772+ // contributes no diagnostic either way; see
773+ // TestProperty_UnquotedDateDefaultPreserved for the default path.
774+ spec := componentSpec (` D:
775+ type: string
776+ format: date
777+ default: 2021-01-01
778+ enum: [2021-01-01, 2022-02-02]
779+ ` )
780+ doc , diags := lowerSpec (t , spec )
781+ assert .Empty (t , diags , "no diagnostics at all: the dates convert cleanly" )
782+ e , ok := doc .Types [componentID ("D" )].(* ir.Enum )
783+ require .True (t , ok , "D stays a closed Enum, never degrades to a Union of literals" )
784+ assert .True (t , e .Closed )
785+ assert .Equal (t , ir .PrimString , e .ValueType )
786+ require .Len (t , e .Members , 2 )
787+ assert .Equal (t , ir.Value {Kind : ir .ValueString , Str : "2021-01-01" }, e .Members [0 ].Value )
788+ assert .Equal (t , ir.Value {Kind : ir .ValueString , Str : "2022-02-02" }, e .Members [1 ].Value )
789+ }
790+
791+ func TestProperty_UnquotedDateDefaultPreserved (t * testing.T ) {
792+ t .Parallel ()
793+ // The repro's default sits at the component level, which nothing lowers by
794+ // itself; this covers the path that actually surfaces the bug in practice —
795+ // a date default declared on an object property.
796+ spec := componentSpec (` S:
797+ type: object
798+ properties:
799+ d:
800+ type: string
801+ format: date
802+ default: 2021-01-01
803+ ` )
804+ doc , diags := lowerSpec (t , spec )
805+ assert .Empty (t , diags , "no diagnostics at all: the date default converts cleanly" )
806+ m , ok := doc .Types [componentID ("S" )].(* ir.Model )
807+ require .True (t , ok )
808+ require .Len (t , m .Properties , 1 )
809+ require .NotNil (t , m .Properties [0 ].Default )
810+ assert .Equal (t , ir.Value {Kind : ir .ValueString , Str : "2021-01-01" }, * m .Properties [0 ].Default )
811+ }
812+
718813func TestAllOf_DiscriminatorHierarchy (t * testing.T ) {
719814 t .Parallel ()
720815 spec := componentSpecVer ("3.2.0" , ` Pet:
0 commit comments