1- import { describe , it } from "vitest" ;
1+ import { describe , it , expect } from "vitest" ;
22import $RefParser from "../../../lib/index.js" ;
33import helper from "../../utils/helper.js" ;
44import path from "../../utils/path.js" ;
55
6- import { expect } from "vitest" ;
7-
86describe ( "Schema with an extensive amount of circular $refs" , ( ) => {
97 it ( "should dereference successfully" , async ( ) => {
108 const circularRefs = new Set < string > ( ) ;
@@ -31,8 +29,9 @@ describe("Schema with an extensive amount of circular $refs", () => {
3129 } ,
3230 } ) ;
3331
34- // Ensure that a circular $ref **was** dereferenced.
35- expect ( circularRefs ) . toHaveLength ( 23 ) ;
32+ // With circular: true (default), circular $refs are replaced with the resolved object.
33+ // onCircular fires for each $ref location pointing to a circular target (118 unique paths).
34+ expect ( circularRefs . size ) . toBe ( 118 ) ;
3635 expect ( schema . components ?. schemas ?. Customer ?. properties ?. customerNode ) . toStrictEqual ( {
3736 type : "array" ,
3837 items : {
@@ -74,8 +73,11 @@ describe("Schema with an extensive amount of circular $refs", () => {
7473 } ,
7574 } ) ;
7675
77- // Ensure that a circular $ref was **not** dereferenced.
78- expect ( circularRefs ) . toHaveLength ( 23 ) ;
76+ // With circular: 'ignore', circular $refs remain as { $ref: "..." } objects.
77+ // onCircular fires for each $ref location (same 118 paths as above), PLUS 55 additional
78+ // "interior paths" - $refs inside circular schemas that get re-encountered when the
79+ // containing schema is accessed from multiple entry points.
80+ expect ( circularRefs . size ) . toBe ( 173 ) ;
7981 expect ( schema . components ?. schemas ?. Customer ?. properties ?. customerNode ) . toStrictEqual ( {
8082 type : "array" ,
8183 items : {
@@ -104,4 +106,54 @@ describe("Schema with an extensive amount of circular $refs", () => {
104106 expect ( parser . $refs . circular ) . to . equal ( true ) ;
105107 }
106108 } ) ;
109+
110+ it ( "should expose path differences between circular: true and circular: 'ignore'" , async ( ) => {
111+ const SCHEMA_PATH = "test/specs/circular-extensive/schema.json" ;
112+
113+ // Collect paths with circular: true (default)
114+ const pathsTrue = new Set < string > ( ) ;
115+ await new $RefParser ( ) . dereference ( path . rel ( SCHEMA_PATH ) , {
116+ dereference : {
117+ onCircular : ( ref : string ) => pathsTrue . add ( ref . split ( "#" ) [ 1 ] ) ,
118+ } ,
119+ } ) ;
120+
121+ // Collect paths with circular: 'ignore'
122+ const pathsIgnore = new Set < string > ( ) ;
123+ await new $RefParser ( ) . dereference ( path . rel ( SCHEMA_PATH ) , {
124+ dereference : {
125+ circular : "ignore" ,
126+ onCircular : ( ref : string ) => pathsIgnore . add ( ref . split ( "#" ) [ 1 ] ) ,
127+ } ,
128+ } ) ;
129+
130+ // Verify the counts
131+ expect ( pathsTrue . size ) . toBe ( 118 ) ;
132+ expect ( pathsIgnore . size ) . toBe ( 173 ) ;
133+
134+ // All paths in 'true' mode should also be in 'ignore' mode
135+ const pathsOnlyInTrue = [ ...pathsTrue ] . filter ( ( p ) => ! pathsIgnore . has ( p ) ) ;
136+ expect ( pathsOnlyInTrue ) . toHaveLength ( 0 ) ;
137+
138+ // 'ignore' mode has 55 additional paths not found in 'true' mode
139+ const pathsOnlyInIgnore = [ ...pathsIgnore ] . filter ( ( p ) => ! pathsTrue . has ( p ) ) ;
140+ expect ( pathsOnlyInIgnore ) . toHaveLength ( 55 ) ;
141+
142+ // These extra paths are "interior paths" within circular schemas that get
143+ // re-visited because $ref objects allow re-entry from different traversal routes.
144+ // With circular: true, these paths aren't reported because the same object
145+ // instance is detected by parents.has() which doesn't trigger onCircular.
146+ //
147+ // Example extra paths (interior of circular schemas reached via different routes):
148+ // Customer contains customerNode.items → CustomerNode (circular).
149+ // - In 'true' mode: Customer.customerNode.items becomes the resolved CustomerNode object.
150+ // When Customer is accessed from another route, customerNode.items is the same object
151+ // instance already in `parents`, so no onCircular fires for that interior path.
152+ // - In 'ignore' mode: Customer.customerNode.items remains { $ref: "..." }.
153+ // When Customer is accessed from another route, the $ref is re-encountered and
154+ // triggers onCircular via cache hit, reporting the interior path.
155+ expect ( pathsOnlyInIgnore ) . toContain ( "/components/schemas/Customer/properties/customerNode/items" ) ;
156+ expect ( pathsOnlyInIgnore ) . toContain ( "/components/schemas/Customer/properties/customerExternalReference/items" ) ;
157+ expect ( pathsOnlyInIgnore ) . toContain ( "/components/schemas/Node/properties/configWcCodeNode/items" ) ;
158+ } ) ;
107159} ) ;
0 commit comments