pushpam14 commited on
Commit
04e4b5b
Β·
verified Β·
1 Parent(s): 709ba83

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. inference.py +49 -17
  2. server/spec_generator.py +343 -0
  3. 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) with these fields:
101
  {
102
- "field_path": "<dot-notation path to the violated field, or 'DONE' if no more violations>",
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
- Rules:
109
- - Report ONE violation per turn.
110
- - Use dot-notation for nested paths: 'customer.email'
111
- - Use bracket notation for arrays: 'items[1].quantity'
112
- - For breaking changes use path format: 'METHOD /path.field' e.g. 'POST /products.price'
113
- - For breaking changes between API versions, ALWAYS use violation_type='breaking_change'.
114
- - For cross-field constraints (arithmetic, date ordering, conditional requirements), use violation_type='cross_field_constraint'.
115
- - The field_path must contain ONLY the path β€” never include the violation_type inside the field_path.
116
- - When you have found all violations, respond with field_path set to 'DONE'.
117
- - Do NOT repeat a violation you already reported.
118
- - You may submit field_path='HINT' to receive a location hint at a cost of -0.5 reward.
 
 
 
 
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
- action_data = query_llm(client, obs_dict, step, history)
 
 
 
 
 
 
 
 
 
 
 
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 test_five_tasks_registered():
29
- assert len(AVAILABLE_TASKS) == 5
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)}"