Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- inference.py +49 -17
- server/spec_generator.py +343 -0
- tests/test_environment.py +27 -2
inference.py
CHANGED
|
@@ -43,6 +43,7 @@ TASKS = [
|
|
| 43 |
"detect_breaking_changes",
|
| 44 |
"validate_response_schema",
|
| 45 |
"validate_cross_field_constraints",
|
|
|
|
| 46 |
]
|
| 47 |
MAX_STEPS_PER_TASK = {
|
| 48 |
"find_type_mismatches": 10,
|
|
@@ -50,7 +51,9 @@ MAX_STEPS_PER_TASK = {
|
|
| 50 |
"detect_breaking_changes": 20,
|
| 51 |
"validate_response_schema": 25,
|
| 52 |
"validate_cross_field_constraints": 18,
|
|
|
|
| 53 |
}
|
|
|
|
| 54 |
TEMPERATURE = 0.2
|
| 55 |
MAX_TOKENS = 1024
|
| 56 |
SUCCESS_SCORE_THRESHOLD = 0.3
|
|
@@ -93,29 +96,33 @@ def log_end(
|
|
| 93 |
|
| 94 |
SYSTEM_PROMPT = textwrap.dedent("""\
|
| 95 |
You are an expert API contract validator. You will be given an OpenAPI \
|
| 96 |
-
specification and an API payload. Your job is to find violations in the \
|
| 97 |
payload that do not conform to the spec.
|
| 98 |
|
| 99 |
Each turn you must respond with EXACTLY one JSON object (no markdown, no \
|
| 100 |
-
explanation outside the JSON)
|
| 101 |
{
|
| 102 |
-
"field_path": "<dot-notation path to the violated field, or 'DONE' if
|
| 103 |
"violation_type": "<type_mismatch|missing_required|invalid_enum|format_error|extra_field|breaking_change|cross_field_constraint>",
|
| 104 |
-
"description": "<brief explanation>",
|
| 105 |
"suggested_fix": "<how to fix it>"
|
| 106 |
}
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
-
|
| 114 |
-
-
|
| 115 |
-
-
|
| 116 |
-
-
|
| 117 |
-
-
|
| 118 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
""")
|
| 120 |
|
| 121 |
|
|
@@ -264,6 +271,8 @@ async def run_single_task(
|
|
| 264 |
steps_taken = 0
|
| 265 |
score = 0.01 # default: strictly > 0 as required by evaluator
|
| 266 |
success = False
|
|
|
|
|
|
|
| 267 |
|
| 268 |
log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME)
|
| 269 |
|
|
@@ -275,7 +284,18 @@ async def run_single_task(
|
|
| 275 |
if result.done:
|
| 276 |
break
|
| 277 |
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
action = ValidatorAction(
|
| 281 |
field_path=action_data["field_path"],
|
|
@@ -297,8 +317,20 @@ async def run_single_task(
|
|
| 297 |
action_str = f"{action_data['field_path']}:{action_data['violation_type']}"
|
| 298 |
log_step(step=step, action=action_str, reward=reward, done=done, error=error)
|
| 299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
history.append(
|
| 301 |
-
f"Step {step}: {action_str} β reward {reward:+.2f}"
|
|
|
|
| 302 |
)
|
| 303 |
|
| 304 |
if done:
|
|
|
|
| 43 |
"detect_breaking_changes",
|
| 44 |
"validate_response_schema",
|
| 45 |
"validate_cross_field_constraints",
|
| 46 |
+
"validate_auth_request",
|
| 47 |
]
|
| 48 |
MAX_STEPS_PER_TASK = {
|
| 49 |
"find_type_mismatches": 10,
|
|
|
|
| 51 |
"detect_breaking_changes": 20,
|
| 52 |
"validate_response_schema": 25,
|
| 53 |
"validate_cross_field_constraints": 18,
|
| 54 |
+
"validate_auth_request": 14,
|
| 55 |
}
|
| 56 |
+
MAX_CONSECUTIVE_FAILURES = 3 # stop retrying same field after this many -0.3 rewards
|
| 57 |
TEMPERATURE = 0.2
|
| 58 |
MAX_TOKENS = 1024
|
| 59 |
SUCCESS_SCORE_THRESHOLD = 0.3
|
|
|
|
| 96 |
|
| 97 |
SYSTEM_PROMPT = textwrap.dedent("""\
|
| 98 |
You are an expert API contract validator. You will be given an OpenAPI \
|
| 99 |
+
specification and an API payload. Your job is to find ALL violations in the \
|
| 100 |
payload that do not conform to the spec.
|
| 101 |
|
| 102 |
Each turn you must respond with EXACTLY one JSON object (no markdown, no \
|
| 103 |
+
explanation outside the JSON):
|
| 104 |
{
|
| 105 |
+
"field_path": "<dot-notation path to the violated field, or 'DONE' if finished>",
|
| 106 |
"violation_type": "<type_mismatch|missing_required|invalid_enum|format_error|extra_field|breaking_change|cross_field_constraint>",
|
| 107 |
+
"description": "<brief explanation of the violation>",
|
| 108 |
"suggested_fix": "<how to fix it>"
|
| 109 |
}
|
| 110 |
|
| 111 |
+
STRICT RULES:
|
| 112 |
+
1. Report ONE violation per turn. Be systematic β check every field.
|
| 113 |
+
2. field_path must be ONLY the path. NEVER put ':violation_type' inside field_path.
|
| 114 |
+
3. Paths: dot-notation 'customer.email', arrays 'items[1].quantity', breaking changes 'POST /path.field'.
|
| 115 |
+
4. violation_type choices:
|
| 116 |
+
- type_mismatch: wrong data type (string vs integer, etc.)
|
| 117 |
+
- missing_required: required field absent from payload
|
| 118 |
+
- invalid_enum: value not in the allowed enum list
|
| 119 |
+
- format_error: value violates format/pattern/min/max constraint
|
| 120 |
+
- breaking_change: API v1βv2 change that breaks existing clients (ALWAYS use this for breaking changes)
|
| 121 |
+
- cross_field_constraint: arithmetic/date/conditional rule across multiple fields
|
| 122 |
+
5. Do NOT repeat a violation already in 'Violations found so far'.
|
| 123 |
+
6. If last feedback was 'False positive' or negative reward, that field is WRONG β move to a different field.
|
| 124 |
+
7. When you have reported all violations, set field_path='DONE'.
|
| 125 |
+
8. You may set field_path='HINT' for a location clue at -0.5 reward cost.
|
| 126 |
""")
|
| 127 |
|
| 128 |
|
|
|
|
| 271 |
steps_taken = 0
|
| 272 |
score = 0.01 # default: strictly > 0 as required by evaluator
|
| 273 |
success = False
|
| 274 |
+
consecutive_failures = 0
|
| 275 |
+
last_failed_path = ""
|
| 276 |
|
| 277 |
log_start(task=task_name, env=BENCHMARK, model=MODEL_NAME)
|
| 278 |
|
|
|
|
| 284 |
if result.done:
|
| 285 |
break
|
| 286 |
|
| 287 |
+
# If stuck on same wrong field too many times, request a HINT
|
| 288 |
+
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
| 289 |
+
action_data = {
|
| 290 |
+
"field_path": "HINT",
|
| 291 |
+
"violation_type": "",
|
| 292 |
+
"description": "",
|
| 293 |
+
"suggested_fix": "",
|
| 294 |
+
}
|
| 295 |
+
consecutive_failures = 0
|
| 296 |
+
last_failed_path = ""
|
| 297 |
+
else:
|
| 298 |
+
action_data = query_llm(client, obs_dict, step, history)
|
| 299 |
|
| 300 |
action = ValidatorAction(
|
| 301 |
field_path=action_data["field_path"],
|
|
|
|
| 317 |
action_str = f"{action_data['field_path']}:{action_data['violation_type']}"
|
| 318 |
log_step(step=step, action=action_str, reward=reward, done=done, error=error)
|
| 319 |
|
| 320 |
+
# Track consecutive failures on same field to trigger HINT
|
| 321 |
+
if reward < 0 and action_data["field_path"] not in ("DONE", "HINT"):
|
| 322 |
+
if action_data["field_path"] == last_failed_path:
|
| 323 |
+
consecutive_failures += 1
|
| 324 |
+
else:
|
| 325 |
+
consecutive_failures = 1
|
| 326 |
+
last_failed_path = action_data["field_path"]
|
| 327 |
+
else:
|
| 328 |
+
consecutive_failures = 0
|
| 329 |
+
last_failed_path = ""
|
| 330 |
+
|
| 331 |
history.append(
|
| 332 |
+
f"Step {step}: {action_str} β reward {reward:+.2f} "
|
| 333 |
+
f"({'correct' if reward >= 1.0 else 'WRONG - do not retry this field' if reward < 0 else 'partial'})"
|
| 334 |
)
|
| 335 |
|
| 336 |
if done:
|
server/spec_generator.py
CHANGED
|
@@ -206,6 +206,50 @@ _EASY_POOL: List[Tuple[str, Any, PlantedViolation]] = [
|
|
| 206 |
actual_value="'ab' (length 2)",
|
| 207 |
),
|
| 208 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
]
|
| 210 |
|
| 211 |
|
|
@@ -1615,6 +1659,304 @@ def generate_cross_field_scenario(seed: Optional[int] = None) -> TaskScenario:
|
|
| 1615 |
)
|
| 1616 |
|
| 1617 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1618 |
# ββ Registry ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1619 |
|
| 1620 |
TASK_GENERATORS = {
|
|
@@ -1623,6 +1965,7 @@ TASK_GENERATORS = {
|
|
| 1623 |
"detect_breaking_changes": generate_hard_scenario,
|
| 1624 |
"validate_response_schema": generate_format_validation_scenario,
|
| 1625 |
"validate_cross_field_constraints": generate_cross_field_scenario,
|
|
|
|
| 1626 |
}
|
| 1627 |
|
| 1628 |
AVAILABLE_TASKS = list(TASK_GENERATORS.keys())
|
|
|
|
| 206 |
actual_value="'ab' (length 2)",
|
| 207 |
),
|
| 208 |
),
|
| 209 |
+
(
|
| 210 |
+
"account_balance",
|
| 211 |
+
"two-fifty",
|
| 212 |
+
PlantedViolation(
|
| 213 |
+
field_path="account_balance",
|
| 214 |
+
violation_type="type_mismatch",
|
| 215 |
+
description="Field 'account_balance' should be number but received string.",
|
| 216 |
+
expected_value="number",
|
| 217 |
+
actual_value="string ('two-fifty')",
|
| 218 |
+
),
|
| 219 |
+
),
|
| 220 |
+
(
|
| 221 |
+
"phone",
|
| 222 |
+
"0014155551234",
|
| 223 |
+
PlantedViolation(
|
| 224 |
+
field_path="phone",
|
| 225 |
+
violation_type="format_error",
|
| 226 |
+
description="Field 'phone' starts with 00 not +; must match E.164 format.",
|
| 227 |
+
expected_value=r"pattern: ^\+[1-9][0-9]{7,14}$",
|
| 228 |
+
actual_value="'0014155551234'",
|
| 229 |
+
),
|
| 230 |
+
),
|
| 231 |
+
(
|
| 232 |
+
"terms_accepted",
|
| 233 |
+
1,
|
| 234 |
+
PlantedViolation(
|
| 235 |
+
field_path="terms_accepted",
|
| 236 |
+
violation_type="type_mismatch",
|
| 237 |
+
description="Field 'terms_accepted' should be boolean but received integer.",
|
| 238 |
+
expected_value="boolean",
|
| 239 |
+
actual_value="integer (1)",
|
| 240 |
+
),
|
| 241 |
+
),
|
| 242 |
+
(
|
| 243 |
+
"username",
|
| 244 |
+
"a" * 33,
|
| 245 |
+
PlantedViolation(
|
| 246 |
+
field_path="username",
|
| 247 |
+
violation_type="format_error",
|
| 248 |
+
description="Field 'username' has length 33, exceeds maxLength of 32.",
|
| 249 |
+
expected_value="string, maxLength: 32",
|
| 250 |
+
actual_value=f"'{('a' * 33)}' (length 33)",
|
| 251 |
+
),
|
| 252 |
+
),
|
| 253 |
]
|
| 254 |
|
| 255 |
|
|
|
|
| 1659 |
)
|
| 1660 |
|
| 1661 |
|
| 1662 |
+
# ββ Expert Task 3 β authentication & security schema validation βββββββββββ
|
| 1663 |
+
#
|
| 1664 |
+
# Validates a complex authentication API payload covering OAuth2 tokens,
|
| 1665 |
+
# permission scopes, rate limits, and security constraints. Two variants
|
| 1666 |
+
# selected by seed parity.
|
| 1667 |
+
|
| 1668 |
+
|
| 1669 |
+
def _auth_scenario_a() -> TaskScenario:
|
| 1670 |
+
"""Auth scenario A: OAuth2 token introspection response with 6 violations."""
|
| 1671 |
+
api_spec: Dict[str, Any] = {
|
| 1672 |
+
"openapi": "3.0.3",
|
| 1673 |
+
"info": {"title": "Auth Service API", "version": "2.0.0"},
|
| 1674 |
+
"paths": {
|
| 1675 |
+
"/auth/token": {
|
| 1676 |
+
"post": {
|
| 1677 |
+
"summary": "Issue an OAuth2 access token",
|
| 1678 |
+
"requestBody": {
|
| 1679 |
+
"required": True,
|
| 1680 |
+
"content": {
|
| 1681 |
+
"application/json": {
|
| 1682 |
+
"schema": {
|
| 1683 |
+
"type": "object",
|
| 1684 |
+
"required": ["client_id", "client_secret", "grant_type", "scope"],
|
| 1685 |
+
"properties": {
|
| 1686 |
+
"client_id": {
|
| 1687 |
+
"type": "string",
|
| 1688 |
+
"pattern": "^[a-zA-Z0-9_-]{8,64}$",
|
| 1689 |
+
},
|
| 1690 |
+
"client_secret": {
|
| 1691 |
+
"type": "string",
|
| 1692 |
+
"minLength": 32,
|
| 1693 |
+
},
|
| 1694 |
+
"grant_type": {
|
| 1695 |
+
"type": "string",
|
| 1696 |
+
"enum": [
|
| 1697 |
+
"authorization_code",
|
| 1698 |
+
"client_credentials",
|
| 1699 |
+
"refresh_token",
|
| 1700 |
+
"password",
|
| 1701 |
+
],
|
| 1702 |
+
},
|
| 1703 |
+
"scope": {
|
| 1704 |
+
"type": "array",
|
| 1705 |
+
"minItems": 1,
|
| 1706 |
+
"items": {
|
| 1707 |
+
"type": "string",
|
| 1708 |
+
"enum": [
|
| 1709 |
+
"read:users",
|
| 1710 |
+
"write:users",
|
| 1711 |
+
"read:orders",
|
| 1712 |
+
"write:orders",
|
| 1713 |
+
"admin",
|
| 1714 |
+
],
|
| 1715 |
+
},
|
| 1716 |
+
},
|
| 1717 |
+
"expires_in": {
|
| 1718 |
+
"type": "integer",
|
| 1719 |
+
"minimum": 60,
|
| 1720 |
+
"maximum": 86400,
|
| 1721 |
+
},
|
| 1722 |
+
"redirect_uri": {
|
| 1723 |
+
"type": "string",
|
| 1724 |
+
"format": "uri",
|
| 1725 |
+
},
|
| 1726 |
+
"mfa_token": {
|
| 1727 |
+
"type": "string",
|
| 1728 |
+
"pattern": "^[0-9]{6}$",
|
| 1729 |
+
"description": "6-digit numeric MFA code",
|
| 1730 |
+
},
|
| 1731 |
+
},
|
| 1732 |
+
}
|
| 1733 |
+
}
|
| 1734 |
+
},
|
| 1735 |
+
},
|
| 1736 |
+
}
|
| 1737 |
+
}
|
| 1738 |
+
},
|
| 1739 |
+
}
|
| 1740 |
+
|
| 1741 |
+
payload: Dict[str, Any] = {
|
| 1742 |
+
"client_id": "abc",
|
| 1743 |
+
"client_secret": "short",
|
| 1744 |
+
"grant_type": "implicit",
|
| 1745 |
+
"scope": ["read:users", "delete:everything"],
|
| 1746 |
+
"expires_in": 0,
|
| 1747 |
+
"redirect_uri": "not-a-valid-uri",
|
| 1748 |
+
"mfa_token": "12AB56",
|
| 1749 |
+
}
|
| 1750 |
+
|
| 1751 |
+
violations = [
|
| 1752 |
+
PlantedViolation(
|
| 1753 |
+
field_path="client_id",
|
| 1754 |
+
violation_type="format_error",
|
| 1755 |
+
description="'abc' has length 3, below minLength pattern requirement of 8 characters.",
|
| 1756 |
+
expected_value="string matching ^[a-zA-Z0-9_-]{8,64}$",
|
| 1757 |
+
actual_value="'abc'",
|
| 1758 |
+
),
|
| 1759 |
+
PlantedViolation(
|
| 1760 |
+
field_path="client_secret",
|
| 1761 |
+
violation_type="format_error",
|
| 1762 |
+
description="'short' has length 5, below minLength of 32.",
|
| 1763 |
+
expected_value="string, minLength: 32",
|
| 1764 |
+
actual_value="'short' (length 5)",
|
| 1765 |
+
),
|
| 1766 |
+
PlantedViolation(
|
| 1767 |
+
field_path="grant_type",
|
| 1768 |
+
violation_type="invalid_enum",
|
| 1769 |
+
description="'implicit' not in allowed enum [authorization_code, client_credentials, refresh_token, password].",
|
| 1770 |
+
expected_value="one of: authorization_code, client_credentials, refresh_token, password",
|
| 1771 |
+
actual_value="'implicit'",
|
| 1772 |
+
),
|
| 1773 |
+
PlantedViolation(
|
| 1774 |
+
field_path="scope[1]",
|
| 1775 |
+
violation_type="invalid_enum",
|
| 1776 |
+
description="'delete:everything' not in allowed scope enum.",
|
| 1777 |
+
expected_value="one of: read:users, write:users, read:orders, write:orders, admin",
|
| 1778 |
+
actual_value="'delete:everything'",
|
| 1779 |
+
),
|
| 1780 |
+
PlantedViolation(
|
| 1781 |
+
field_path="expires_in",
|
| 1782 |
+
violation_type="format_error",
|
| 1783 |
+
description="'expires_in' is 0, below minimum of 60.",
|
| 1784 |
+
expected_value="integer, minimum: 60",
|
| 1785 |
+
actual_value="0",
|
| 1786 |
+
),
|
| 1787 |
+
PlantedViolation(
|
| 1788 |
+
field_path="mfa_token",
|
| 1789 |
+
violation_type="format_error",
|
| 1790 |
+
description="'12AB56' contains letters; must match ^[0-9]{6}$ (6 digits only).",
|
| 1791 |
+
expected_value="string matching ^[0-9]{6}$",
|
| 1792 |
+
actual_value="'12AB56'",
|
| 1793 |
+
),
|
| 1794 |
+
]
|
| 1795 |
+
|
| 1796 |
+
return TaskScenario(
|
| 1797 |
+
task_name="validate_auth_request",
|
| 1798 |
+
task_description=(
|
| 1799 |
+
"You are given an OpenAPI specification for POST /auth/token (OAuth2 token issuance) "
|
| 1800 |
+
"and an API request payload. Find all violations: invalid enum values for grant_type "
|
| 1801 |
+
"and scope items, format/pattern violations (client_id pattern, client_secret length, "
|
| 1802 |
+
"mfa_token digits-only, redirect_uri URI format), and out-of-range numeric values. "
|
| 1803 |
+
"Use dot-notation for nested paths and bracket notation for arrays (e.g. 'scope[1]'). "
|
| 1804 |
+
"Submit field_path='DONE' when finished."
|
| 1805 |
+
),
|
| 1806 |
+
api_spec=api_spec,
|
| 1807 |
+
payload=payload,
|
| 1808 |
+
violations=violations,
|
| 1809 |
+
max_steps=14,
|
| 1810 |
+
)
|
| 1811 |
+
|
| 1812 |
+
|
| 1813 |
+
def _auth_scenario_b() -> TaskScenario:
|
| 1814 |
+
"""Auth scenario B: API key management request with 6 different violations."""
|
| 1815 |
+
api_spec: Dict[str, Any] = {
|
| 1816 |
+
"openapi": "3.0.3",
|
| 1817 |
+
"info": {"title": "API Key Management", "version": "1.0.0"},
|
| 1818 |
+
"paths": {
|
| 1819 |
+
"/api-keys": {
|
| 1820 |
+
"post": {
|
| 1821 |
+
"summary": "Create a new API key",
|
| 1822 |
+
"requestBody": {
|
| 1823 |
+
"required": True,
|
| 1824 |
+
"content": {
|
| 1825 |
+
"application/json": {
|
| 1826 |
+
"schema": {
|
| 1827 |
+
"type": "object",
|
| 1828 |
+
"required": ["name", "permissions", "environment", "ttl_days"],
|
| 1829 |
+
"properties": {
|
| 1830 |
+
"name": {
|
| 1831 |
+
"type": "string",
|
| 1832 |
+
"minLength": 3,
|
| 1833 |
+
"maxLength": 50,
|
| 1834 |
+
"pattern": "^[a-z0-9-]+$",
|
| 1835 |
+
"description": "Lowercase alphanumeric with hyphens only",
|
| 1836 |
+
},
|
| 1837 |
+
"permissions": {
|
| 1838 |
+
"type": "array",
|
| 1839 |
+
"minItems": 1,
|
| 1840 |
+
"maxItems": 5,
|
| 1841 |
+
"items": {
|
| 1842 |
+
"type": "string",
|
| 1843 |
+
"enum": ["read", "write", "delete", "admin"],
|
| 1844 |
+
},
|
| 1845 |
+
},
|
| 1846 |
+
"environment": {
|
| 1847 |
+
"type": "string",
|
| 1848 |
+
"enum": ["development", "staging", "production"],
|
| 1849 |
+
},
|
| 1850 |
+
"ttl_days": {
|
| 1851 |
+
"type": "integer",
|
| 1852 |
+
"minimum": 1,
|
| 1853 |
+
"maximum": 365,
|
| 1854 |
+
},
|
| 1855 |
+
"ip_whitelist": {
|
| 1856 |
+
"type": "array",
|
| 1857 |
+
"items": {
|
| 1858 |
+
"type": "string",
|
| 1859 |
+
"format": "ipv4",
|
| 1860 |
+
},
|
| 1861 |
+
},
|
| 1862 |
+
"rate_limit": {
|
| 1863 |
+
"type": "integer",
|
| 1864 |
+
"minimum": 10,
|
| 1865 |
+
"maximum": 10000,
|
| 1866 |
+
"description": "Requests per minute",
|
| 1867 |
+
},
|
| 1868 |
+
},
|
| 1869 |
+
}
|
| 1870 |
+
}
|
| 1871 |
+
},
|
| 1872 |
+
},
|
| 1873 |
+
}
|
| 1874 |
+
}
|
| 1875 |
+
},
|
| 1876 |
+
}
|
| 1877 |
+
|
| 1878 |
+
payload: Dict[str, Any] = {
|
| 1879 |
+
"name": "My API Key!",
|
| 1880 |
+
"permissions": ["read", "write", "superuser"],
|
| 1881 |
+
"environment": "local",
|
| 1882 |
+
"ttl_days": 0,
|
| 1883 |
+
"ip_whitelist": ["192.168.1.1", "999.0.0.1"],
|
| 1884 |
+
"rate_limit": 5,
|
| 1885 |
+
}
|
| 1886 |
+
|
| 1887 |
+
violations = [
|
| 1888 |
+
PlantedViolation(
|
| 1889 |
+
field_path="name",
|
| 1890 |
+
violation_type="format_error",
|
| 1891 |
+
description="'My API Key!' contains spaces and '!'; must match ^[a-z0-9-]+$ (lowercase, digits, hyphens only).",
|
| 1892 |
+
expected_value="string matching ^[a-z0-9-]+$",
|
| 1893 |
+
actual_value="'My API Key!'",
|
| 1894 |
+
),
|
| 1895 |
+
PlantedViolation(
|
| 1896 |
+
field_path="permissions[2]",
|
| 1897 |
+
violation_type="invalid_enum",
|
| 1898 |
+
description="'superuser' not in allowed enum [read, write, delete, admin].",
|
| 1899 |
+
expected_value="one of: read, write, delete, admin",
|
| 1900 |
+
actual_value="'superuser'",
|
| 1901 |
+
),
|
| 1902 |
+
PlantedViolation(
|
| 1903 |
+
field_path="environment",
|
| 1904 |
+
violation_type="invalid_enum",
|
| 1905 |
+
description="'local' not in enum [development, staging, production].",
|
| 1906 |
+
expected_value="one of: development, staging, production",
|
| 1907 |
+
actual_value="'local'",
|
| 1908 |
+
),
|
| 1909 |
+
PlantedViolation(
|
| 1910 |
+
field_path="ttl_days",
|
| 1911 |
+
violation_type="format_error",
|
| 1912 |
+
description="'ttl_days' is 0, below minimum of 1.",
|
| 1913 |
+
expected_value="integer, minimum: 1",
|
| 1914 |
+
actual_value="0",
|
| 1915 |
+
),
|
| 1916 |
+
PlantedViolation(
|
| 1917 |
+
field_path="ip_whitelist[1]",
|
| 1918 |
+
violation_type="format_error",
|
| 1919 |
+
description="'999.0.0.1' is not a valid IPv4 address (first octet 999 > 255).",
|
| 1920 |
+
expected_value="string, format: ipv4",
|
| 1921 |
+
actual_value="'999.0.0.1'",
|
| 1922 |
+
),
|
| 1923 |
+
PlantedViolation(
|
| 1924 |
+
field_path="rate_limit",
|
| 1925 |
+
violation_type="format_error",
|
| 1926 |
+
description="'rate_limit' is 5, below minimum of 10.",
|
| 1927 |
+
expected_value="integer, minimum: 10",
|
| 1928 |
+
actual_value="5",
|
| 1929 |
+
),
|
| 1930 |
+
]
|
| 1931 |
+
|
| 1932 |
+
return TaskScenario(
|
| 1933 |
+
task_name="validate_auth_request",
|
| 1934 |
+
task_description=(
|
| 1935 |
+
"You are given an OpenAPI specification for POST /api-keys (API key creation) "
|
| 1936 |
+
"and a request payload. Find all violations: invalid enum values for permissions "
|
| 1937 |
+
"items and environment, format/pattern violations (name pattern, ip_whitelist format), "
|
| 1938 |
+
"and out-of-range numeric values (ttl_days, rate_limit). "
|
| 1939 |
+
"Use dot-notation for nested paths and bracket notation for arrays (e.g. 'permissions[2]'). "
|
| 1940 |
+
"Submit field_path='DONE' when finished."
|
| 1941 |
+
),
|
| 1942 |
+
api_spec=api_spec,
|
| 1943 |
+
payload=payload,
|
| 1944 |
+
violations=violations,
|
| 1945 |
+
max_steps=14,
|
| 1946 |
+
)
|
| 1947 |
+
|
| 1948 |
+
|
| 1949 |
+
def generate_auth_scenario(seed: Optional[int] = None) -> TaskScenario:
|
| 1950 |
+
"""Auth scenario: two variants selected by seed parity.
|
| 1951 |
+
|
| 1952 |
+
seed=None or even β OAuth2 token (variant A).
|
| 1953 |
+
Odd seed β API key management (variant B).
|
| 1954 |
+
"""
|
| 1955 |
+
if seed is None or seed % 2 == 0:
|
| 1956 |
+
return _auth_scenario_a()
|
| 1957 |
+
return _auth_scenario_b()
|
| 1958 |
+
|
| 1959 |
+
|
| 1960 |
# ββ Registry ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1961 |
|
| 1962 |
TASK_GENERATORS = {
|
|
|
|
| 1965 |
"detect_breaking_changes": generate_hard_scenario,
|
| 1966 |
"validate_response_schema": generate_format_validation_scenario,
|
| 1967 |
"validate_cross_field_constraints": generate_cross_field_scenario,
|
| 1968 |
+
"validate_auth_request": generate_auth_scenario,
|
| 1969 |
}
|
| 1970 |
|
| 1971 |
AVAILABLE_TASKS = list(TASK_GENERATORS.keys())
|
tests/test_environment.py
CHANGED
|
@@ -25,14 +25,15 @@ def env():
|
|
| 25 |
# ββ Task structure βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
|
| 27 |
|
| 28 |
-
def
|
| 29 |
-
assert len(AVAILABLE_TASKS) ==
|
| 30 |
expected = {
|
| 31 |
"find_type_mismatches",
|
| 32 |
"validate_nested_objects",
|
| 33 |
"detect_breaking_changes",
|
| 34 |
"validate_response_schema",
|
| 35 |
"validate_cross_field_constraints",
|
|
|
|
| 36 |
}
|
| 37 |
assert set(AVAILABLE_TASKS) == expected
|
| 38 |
|
|
@@ -189,3 +190,27 @@ def test_cross_field_violations_use_correct_type():
|
|
| 189 |
assert v.violation_type == "cross_field_constraint", (
|
| 190 |
f"Expected cross_field_constraint, got {v.violation_type} for {v.field_path}"
|
| 191 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# ββ Task structure βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
|
| 27 |
|
| 28 |
+
def test_six_tasks_registered():
|
| 29 |
+
assert len(AVAILABLE_TASKS) == 6
|
| 30 |
expected = {
|
| 31 |
"find_type_mismatches",
|
| 32 |
"validate_nested_objects",
|
| 33 |
"detect_breaking_changes",
|
| 34 |
"validate_response_schema",
|
| 35 |
"validate_cross_field_constraints",
|
| 36 |
+
"validate_auth_request",
|
| 37 |
}
|
| 38 |
assert set(AVAILABLE_TASKS) == expected
|
| 39 |
|
|
|
|
| 190 |
assert v.violation_type == "cross_field_constraint", (
|
| 191 |
f"Expected cross_field_constraint, got {v.violation_type} for {v.field_path}"
|
| 192 |
)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
# ββ Auth task ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def test_auth_task_has_six_violations():
|
| 199 |
+
scenario = generate_scenario_for_task("validate_auth_request")
|
| 200 |
+
assert len(scenario.violations) == 6
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def test_auth_task_variants_differ():
|
| 204 |
+
s_even = generate_scenario_for_task("validate_auth_request", seed=0)
|
| 205 |
+
s_odd = generate_scenario_for_task("validate_auth_request", seed=1)
|
| 206 |
+
paths_even = {v.field_path for v in s_even.violations}
|
| 207 |
+
paths_odd = {v.field_path for v in s_odd.violations}
|
| 208 |
+
assert paths_even != paths_odd, "Even and odd seed should give different auth scenarios"
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# ββ Easy pool expansion ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def test_easy_pool_has_twelve_variants():
|
| 215 |
+
from server.spec_generator import _EASY_POOL
|
| 216 |
+
assert len(_EASY_POOL) == 12, f"Expected 12 pool entries, got {len(_EASY_POOL)}"
|