@@ -188,11 +188,13 @@ def mutate(spec: dict, n: int, rng: random.Random) -> list[str]:
188188
189189def resolve_refs (spec : dict ) -> dict :
190190 """Recursively resolve all $ref pointers in the spec (in-place)."""
191- def _resolve (node , root ):
191+ def _resolve (node , root , seen_refs ):
192192 if isinstance (node , dict ):
193193 if "$ref" in node and isinstance (node ["$ref" ], str ):
194194 ref = node ["$ref" ]
195195 if ref .startswith ("#/" ):
196+ if ref in seen_refs :
197+ return node
196198 parts = ref [2 :].split ("/" )
197199 target = root
198200 for p in parts :
@@ -203,13 +205,13 @@ def _resolve(node, root):
203205 return node
204206 if isinstance (target , dict ):
205207 resolved = copy .deepcopy (target )
206- return _resolve (resolved , root )
208+ return _resolve (resolved , root , seen_refs | { ref } )
207209 return node
208- return {k : _resolve (v , root ) for k , v in node .items ()}
210+ return {k : _resolve (v , root , seen_refs ) for k , v in node .items ()}
209211 if isinstance (node , list ):
210- return [_resolve (item , root ) for item in node ]
212+ return [_resolve (item , root , seen_refs ) for item in node ]
211213 return node
212- return _resolve (spec , spec )
214+ return _resolve (spec , spec , set () )
213215
214216
215217# Case generation -------------------------------------------------------------
@@ -283,6 +285,15 @@ def sample_value(schema: dict, rng: random.Random, depth: int = 0):
283285 return None
284286
285287
288+ def _merged_parameters (path_item : dict , op : dict ) -> list [dict ]:
289+ """Merge path-item and operation-level parameters (op overrides)."""
290+ merged = {}
291+ for p in (path_item .get ("parameters" ) or []) + (op .get ("parameters" ) or []):
292+ if isinstance (p , dict ):
293+ merged [(p .get ("name" ), p .get ("in" ))] = p
294+ return list (merged .values ())
295+
296+
286297def gen_cases (spec : dict , rng : random .Random , max_per_op : int = 2 ) -> list [dict ]:
287298 """Generate a small set of positive requests per op (oracle: must accept)."""
288299 cases = []
@@ -299,10 +310,12 @@ def gen_cases(spec: dict, rng: random.Random, max_per_op: int = 2) -> list[dict]
299310 break
300311 op_count += 1
301312
302- # Concrete path: substitute each {token} with a small value matching
303- # any declared path-parameter schema.
304- path_params = {p ["name" ]: p for p in (op .get ("parameters" ) or [])
305- if isinstance (p , dict ) and p .get ("in" ) == "path" }
313+ params = _merged_parameters (item , op )
314+ path_params = {
315+ p ["name" ]: p
316+ for p in params
317+ if p .get ("in" ) == "path" and isinstance (p .get ("name" ), str )
318+ }
306319 concrete = path
307320 for token in (s .split ("}" , 1 )[0 ] for s in path .split ("{" )[1 :]):
308321 pp = path_params .get (token , {})
@@ -319,8 +332,7 @@ def gen_cases(spec: dict, rng: random.Random, max_per_op: int = 2) -> list[dict]
319332 "headers" : {},
320333 }
321334
322- # Required query/header params: fill with conforming values
323- for p in op .get ("parameters" ) or []:
335+ for p in params :
324336 if not isinstance (p , dict ):
325337 continue
326338 if not p .get ("required" ):
@@ -413,11 +425,15 @@ def run_validator(spec: dict, cases: list[dict], deps: str, lib: str,
413425 except subprocess .TimeoutExpired :
414426 return [{"phase" : "timeout" , "stderr" : "validator subprocess timed out" }]
415427 out = []
428+ bad_lines = 0
416429 for line in r .stdout .splitlines ():
417430 try :
418431 out .append (json .loads (line ))
419- except Exception :
420- pass
432+ except json .JSONDecodeError :
433+ bad_lines += 1
434+ if bad_lines and not out :
435+ out .append ({"phase" : "subprocess_error" , "rc" : r .returncode ,
436+ "stderr" : f"malformed validator JSONL output ({ bad_lines } lines)" })
421437 if r .returncode != 0 and not out :
422438 out .append ({"phase" : "subprocess_error" , "rc" : r .returncode ,
423439 "stderr" : r .stderr [- 2000 :]})
0 commit comments