ai-testing-swarm 0.1.15__tar.gz → 0.1.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/PKG-INFO +47 -2
  2. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/README.md +44 -1
  3. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/pyproject.toml +4 -1
  4. ai_testing_swarm-0.1.17/src/ai_testing_swarm/__init__.py +1 -0
  5. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/test_planner_agent.py +52 -1
  6. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/cli.py +126 -22
  7. ai_testing_swarm-0.1.17/src/ai_testing_swarm/core/auth_matrix.py +93 -0
  8. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/config.py +6 -0
  9. ai_testing_swarm-0.1.17/src/ai_testing_swarm/core/openapi_fuzzer.py +396 -0
  10. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/risk.py +22 -0
  11. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/orchestrator.py +15 -1
  12. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/reporting/dashboard.py +35 -5
  13. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/reporting/report_writer.py +73 -0
  14. ai_testing_swarm-0.1.17/src/ai_testing_swarm/reporting/trend.py +110 -0
  15. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/PKG-INFO +47 -2
  16. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/SOURCES.txt +5 -0
  17. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/requires.txt +3 -0
  18. ai_testing_swarm-0.1.17/tests/test_batch2_trend_and_auth.py +73 -0
  19. ai_testing_swarm-0.1.17/tests/test_openapi_fuzzer.py +91 -0
  20. ai_testing_swarm-0.1.15/src/ai_testing_swarm/__init__.py +0 -1
  21. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/setup.cfg +0 -0
  22. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/__init__.py +0 -0
  23. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/execution_agent.py +0 -0
  24. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/learning_agent.py +0 -0
  25. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/llm_reasoning_agent.py +0 -0
  26. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/release_gate_agent.py +0 -0
  27. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/test_writer_agent.py +0 -0
  28. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/ui_agent.py +0 -0
  29. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/__init__.py +0 -0
  30. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/api_client.py +0 -0
  31. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/curl_parser.py +0 -0
  32. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/openai_client.py +0 -0
  33. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/openapi_loader.py +0 -0
  34. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/openapi_validator.py +0 -0
  35. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/safety.py +0 -0
  36. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/reporting/__init__.py +0 -0
  37. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/dependency_links.txt +0 -0
  38. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/entry_points.txt +0 -0
  39. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/top_level.txt +0 -0
  40. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/tests/test_openapi_loader.py +0 -0
  41. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/tests/test_openapi_validator.py +0 -0
  42. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/tests/test_policy_expected_negatives.py +0 -0
  43. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/tests/test_risk_scoring_and_gate.py +0 -0
  44. {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/tests/test_swarm_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-testing-swarm
3
- Version: 0.1.15
3
+ Version: 0.1.17
4
4
  Summary: AI-powered testing swarm
5
5
  Author-email: Arif Shah <ashah7775@gmail.com>
6
6
  License: MIT
@@ -10,6 +10,8 @@ Requires-Dist: requests>=2.28
10
10
  Requires-Dist: PyYAML>=6.0
11
11
  Provides-Extra: openapi
12
12
  Requires-Dist: jsonschema>=4.0; extra == "openapi"
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
15
 
14
16
  # AI Testing Swarm
15
17
 
@@ -70,7 +72,9 @@ A report is written under:
70
72
  - `./ai_swarm_reports/<METHOD>_<endpoint>/<METHOD>_<endpoint>_<timestamp>.<json|md|html>`
71
73
 
72
74
  Reports include:
73
- - per-test results
75
+ - per-test results (including deterministic `risk_score` 0..100)
76
+ - endpoint-level risk gate (`PASS`/`WARN`/`BLOCK`)
77
+ - trend vs previous run for the same endpoint (risk delta + regressions)
74
78
  - summary counts by status code / failure type
75
79
  - optional AI summary (if enabled)
76
80
 
@@ -115,6 +119,10 @@ Reports include:
115
119
  - Base URL is read from `spec.servers[0].url`.
116
120
  - When using OpenAPI input, the swarm will also *optionally* validate response status codes against `operation.responses`.
117
121
  - If `jsonschema` is installed (via `ai-testing-swarm[openapi]`) and the response is JSON, response bodies are validated against the OpenAPI `application/json` schema.
122
+ - If an OpenAPI requestBody schema exists for the operation, the planner can also generate **schema-based request-body fuzz cases**:
123
+ - schema-valid edge cases (boundary values that should still validate)
124
+ - schema-invalid variants (missing required fields, wrong types, out-of-range, etc.)
125
+ - Controlled via CLI flags like `--no-openapi-fuzz` / `--openapi-fuzz-max-*`.
118
126
  - Override with `AI_SWARM_OPENAPI_BASE_URL` if your spec doesn’t include servers.
119
127
 
120
128
  ---
@@ -140,6 +148,28 @@ Then generates broad coverage across:
140
148
 
141
149
  ---
142
150
 
151
+ ## Auth matrix runner (multiple tokens/headers)
152
+
153
+ To run the *same* request under multiple auth contexts (e.g., user/admin tokens), create `auth_matrix.yaml`:
154
+
155
+ ```yaml
156
+ cases:
157
+ - name: user
158
+ headers:
159
+ Authorization: "Bearer USER_TOKEN"
160
+ - name: admin
161
+ headers:
162
+ Authorization: "Bearer ADMIN_TOKEN"
163
+ ```
164
+
165
+ Run:
166
+
167
+ ```bash
168
+ ai-test --input request.json --auth-matrix auth_matrix.yaml
169
+ ```
170
+
171
+ Each auth case is written as a separate report using a `run_label` suffix (e.g. `__auth-user`).
172
+
143
173
  ## Safety mode (recommended for CI/demos)
144
174
 
145
175
  Mutation testing can be noisy and may accidentally stress a real environment.
@@ -191,6 +221,21 @@ Reports include:
191
221
  - `summary.counts_by_failure_type`
192
222
  - `summary.counts_by_status_code`
193
223
  - `summary.slow_tests` (based on SLA)
224
+ - `meta.endpoint_risk_score` + `meta.gate_status`
225
+ - `trend.*` (previous comparison if a prior report exists)
226
+
227
+ A static dashboard index is generated at:
228
+ - `./ai_swarm_reports/index.html` (latest JSON report per endpoint, sorted by regressions/risk)
229
+
230
+ ### Regression gate (CI)
231
+
232
+ To fail CI when the run regresses vs the previous JSON report for the same endpoint:
233
+
234
+ ```bash
235
+ ai-test --input request.json --fail-on-regression
236
+ ```
237
+
238
+ This checks `report.trend.regression_count` and exits with a non-zero code if regressions are detected.
194
239
 
195
240
  SLA threshold:
196
241
  - `AI_SWARM_SLA_MS` (default: `2000`)
@@ -57,7 +57,9 @@ A report is written under:
57
57
  - `./ai_swarm_reports/<METHOD>_<endpoint>/<METHOD>_<endpoint>_<timestamp>.<json|md|html>`
58
58
 
59
59
  Reports include:
60
- - per-test results
60
+ - per-test results (including deterministic `risk_score` 0..100)
61
+ - endpoint-level risk gate (`PASS`/`WARN`/`BLOCK`)
62
+ - trend vs previous run for the same endpoint (risk delta + regressions)
61
63
  - summary counts by status code / failure type
62
64
  - optional AI summary (if enabled)
63
65
 
@@ -102,6 +104,10 @@ Reports include:
102
104
  - Base URL is read from `spec.servers[0].url`.
103
105
  - When using OpenAPI input, the swarm will also *optionally* validate response status codes against `operation.responses`.
104
106
  - If `jsonschema` is installed (via `ai-testing-swarm[openapi]`) and the response is JSON, response bodies are validated against the OpenAPI `application/json` schema.
107
+ - If an OpenAPI requestBody schema exists for the operation, the planner can also generate **schema-based request-body fuzz cases**:
108
+ - schema-valid edge cases (boundary values that should still validate)
109
+ - schema-invalid variants (missing required fields, wrong types, out-of-range, etc.)
110
+ - Controlled via CLI flags like `--no-openapi-fuzz` / `--openapi-fuzz-max-*`.
105
111
  - Override with `AI_SWARM_OPENAPI_BASE_URL` if your spec doesn’t include servers.
106
112
 
107
113
  ---
@@ -127,6 +133,28 @@ Then generates broad coverage across:
127
133
 
128
134
  ---
129
135
 
136
+ ## Auth matrix runner (multiple tokens/headers)
137
+
138
+ To run the *same* request under multiple auth contexts (e.g., user/admin tokens), create `auth_matrix.yaml`:
139
+
140
+ ```yaml
141
+ cases:
142
+ - name: user
143
+ headers:
144
+ Authorization: "Bearer USER_TOKEN"
145
+ - name: admin
146
+ headers:
147
+ Authorization: "Bearer ADMIN_TOKEN"
148
+ ```
149
+
150
+ Run:
151
+
152
+ ```bash
153
+ ai-test --input request.json --auth-matrix auth_matrix.yaml
154
+ ```
155
+
156
+ Each auth case is written as a separate report using a `run_label` suffix (e.g. `__auth-user`).
157
+
130
158
  ## Safety mode (recommended for CI/demos)
131
159
 
132
160
  Mutation testing can be noisy and may accidentally stress a real environment.
@@ -178,6 +206,21 @@ Reports include:
178
206
  - `summary.counts_by_failure_type`
179
207
  - `summary.counts_by_status_code`
180
208
  - `summary.slow_tests` (based on SLA)
209
+ - `meta.endpoint_risk_score` + `meta.gate_status`
210
+ - `trend.*` (previous comparison if a prior report exists)
211
+
212
+ A static dashboard index is generated at:
213
+ - `./ai_swarm_reports/index.html` (latest JSON report per endpoint, sorted by regressions/risk)
214
+
215
+ ### Regression gate (CI)
216
+
217
+ To fail CI when the run regresses vs the previous JSON report for the same endpoint:
218
+
219
+ ```bash
220
+ ai-test --input request.json --fail-on-regression
221
+ ```
222
+
223
+ This checks `report.trend.regression_count` and exits with a non-zero code if regressions are detected.
181
224
 
182
225
  SLA threshold:
183
226
  - `AI_SWARM_SLA_MS` (default: `2000`)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-testing-swarm"
7
- version = "0.1.15"
7
+ version = "0.1.17"
8
8
  description = "AI-powered testing swarm"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -22,6 +22,9 @@ dependencies = [
22
22
  openapi = [
23
23
  "jsonschema>=4.0",
24
24
  ]
25
+ dev = [
26
+ "pytest>=8.0",
27
+ ]
25
28
 
26
29
  [project.scripts]
27
30
  ai-test = "ai_testing_swarm.cli:main"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.17"
@@ -363,7 +363,58 @@ class TestPlannerAgent:
363
363
  for key, value in body.items():
364
364
  add_field_mutations("body", key, value)
365
365
 
366
- # 6) Optional OpenAI augmentation (best-effort)
366
+ # 6) Optional OpenAPI schema-based request-body fuzzing (best-effort)
367
+ try:
368
+ from ai_testing_swarm.core.config import (
369
+ AI_SWARM_OPENAPI_FUZZ,
370
+ AI_SWARM_OPENAPI_FUZZ_MAX_DEPTH,
371
+ AI_SWARM_OPENAPI_FUZZ_MAX_INVALID,
372
+ AI_SWARM_OPENAPI_FUZZ_MAX_VALID,
373
+ )
374
+
375
+ if AI_SWARM_OPENAPI_FUZZ and isinstance(request.get("_openapi"), dict):
376
+ ctx = request.get("_openapi") or {}
377
+ spec = ctx.get("spec")
378
+ path0 = ctx.get("path")
379
+ method0 = ctx.get("method")
380
+
381
+ if isinstance(spec, dict) and isinstance(path0, str) and isinstance(method0, str):
382
+ from ai_testing_swarm.core.openapi_fuzzer import generate_request_body_fuzz_cases
383
+
384
+ fuzz_cases = generate_request_body_fuzz_cases(
385
+ spec=spec,
386
+ path=path0,
387
+ method=method0,
388
+ max_valid=AI_SWARM_OPENAPI_FUZZ_MAX_VALID,
389
+ max_invalid=AI_SWARM_OPENAPI_FUZZ_MAX_INVALID,
390
+ max_depth=AI_SWARM_OPENAPI_FUZZ_MAX_DEPTH,
391
+ )
392
+
393
+ existing = {t["name"] for t in tests}
394
+ for fc in fuzz_cases:
395
+ name = fc.name
396
+ if name in existing:
397
+ continue
398
+ tests.append(
399
+ {
400
+ "name": name,
401
+ "mutation": {
402
+ "target": "body_whole",
403
+ "path": [],
404
+ "operation": "REPLACE",
405
+ "original_value": body,
406
+ "new_value": fc.body,
407
+ "strategy": "openapi_fuzz_valid" if fc.is_valid else "openapi_fuzz_invalid",
408
+ "openapi": {"is_valid": fc.is_valid, "details": fc.details},
409
+ },
410
+ }
411
+ )
412
+ existing.add(name)
413
+ except Exception:
414
+ # Never fail planning due to fuzzing issues
415
+ pass
416
+
417
+ # 7) Optional OpenAI augmentation (best-effort)
367
418
  try:
368
419
  from ai_testing_swarm.core.config import (
369
420
  AI_SWARM_MAX_AI_TESTS,
@@ -107,6 +107,54 @@ def main():
107
107
  help="Report format to write (default: json)",
108
108
  )
109
109
 
110
+ # OpenAPI schema-based request-body fuzzing
111
+ parser.add_argument(
112
+ "--openapi-fuzz",
113
+ dest="openapi_fuzz",
114
+ action="store_true",
115
+ default=True,
116
+ help="Enable OpenAPI schema-based request-body fuzzing when OpenAPI context is present (default: on)",
117
+ )
118
+ parser.add_argument(
119
+ "--no-openapi-fuzz",
120
+ dest="openapi_fuzz",
121
+ action="store_false",
122
+ help="Disable OpenAPI schema-based request-body fuzzing",
123
+ )
124
+ parser.add_argument(
125
+ "--openapi-fuzz-max-valid",
126
+ type=int,
127
+ default=8,
128
+ help="Max schema-valid OpenAPI fuzz cases to add (default: 8)",
129
+ )
130
+ parser.add_argument(
131
+ "--openapi-fuzz-max-invalid",
132
+ type=int,
133
+ default=8,
134
+ help="Max schema-invalid OpenAPI fuzz cases to add (default: 8)",
135
+ )
136
+ parser.add_argument(
137
+ "--openapi-fuzz-max-depth",
138
+ type=int,
139
+ default=6,
140
+ help="Max recursion depth for OpenAPI schema instance generation (default: 6)",
141
+ )
142
+
143
+ parser.add_argument(
144
+ "--fail-on-regression",
145
+ action="store_true",
146
+ help="Exit non-zero if the run regresses vs the previous JSON report for this endpoint",
147
+ )
148
+
149
+ parser.add_argument(
150
+ "--auth-matrix",
151
+ default="",
152
+ help=(
153
+ "Optional path to auth_matrix.yaml/json to run the same endpoint under multiple auth headers. "
154
+ "Each case is reported separately via a run label suffix."
155
+ ),
156
+ )
157
+
110
158
  # Batch1: risk gate thresholds (backward compatible defaults)
111
159
  parser.add_argument(
112
160
  "--gate-warn",
@@ -137,33 +185,89 @@ def main():
137
185
  # ------------------------------------------------------------
138
186
  # Run swarm
139
187
  # ------------------------------------------------------------
188
+ import os
189
+
140
190
  if args.public_only:
141
- import os
142
191
  os.environ["AI_SWARM_PUBLIC_ONLY"] = "1"
143
192
 
144
- decision, results = SwarmOrchestrator().run(
145
- request,
146
- report_format=args.report_format,
147
- gate_warn=args.gate_warn,
148
- gate_block=args.gate_block,
149
- )
193
+ # CLI flags -> env toggles used by core.config
194
+ os.environ["AI_SWARM_OPENAPI_FUZZ"] = "1" if args.openapi_fuzz else "0"
195
+ os.environ["AI_SWARM_OPENAPI_FUZZ_MAX_VALID"] = str(int(args.openapi_fuzz_max_valid))
196
+ os.environ["AI_SWARM_OPENAPI_FUZZ_MAX_INVALID"] = str(int(args.openapi_fuzz_max_invalid))
197
+ os.environ["AI_SWARM_OPENAPI_FUZZ_MAX_DEPTH"] = str(int(args.openapi_fuzz_max_depth))
150
198
 
151
- # ------------------------------------------------------------
152
- # Console output
153
- # ------------------------------------------------------------
154
- print("\n=== RELEASE DECISION ===")
155
- print(decision)
156
-
157
- print("\n=== TEST RESULTS ===")
158
- for r in results:
159
- response = r.get("response", {})
160
- status_code = response.get("status_code")
161
-
162
- print(
163
- f"{r.get('name'):25} "
164
- f"{str(status_code):5} "
165
- f"{r.get('reason')}"
199
+ orch = SwarmOrchestrator()
200
+
201
+ def _print_console(decision, results, *, label: str = ""):
202
+ if label:
203
+ print(f"\n=== AUTH CASE: {label} ===")
204
+ print("\n=== RELEASE DECISION ===")
205
+ print(decision)
206
+ print("\n=== TEST RESULTS ===")
207
+ for r in results:
208
+ response = r.get("response", {})
209
+ status_code = response.get("status_code")
210
+ print(f"{r.get('name'):25} {str(status_code):5} {r.get('reason')}")
211
+
212
+ def _maybe_fail_on_regression(report_path: str):
213
+ try:
214
+ with open(report_path, "r", encoding="utf-8") as f:
215
+ report = json.load(f)
216
+ except Exception:
217
+ return
218
+
219
+ trend = report.get("trend") or {}
220
+ regression_count = trend.get("regression_count")
221
+ try:
222
+ reg_i = int(regression_count or 0)
223
+ except Exception:
224
+ reg_i = 0
225
+
226
+ if reg_i > 0:
227
+ print(f"\n❌ Regression gate failed: regressions={reg_i} (see trend in report)")
228
+ raise SystemExit(2)
229
+
230
+ if args.auth_matrix:
231
+ from ai_testing_swarm.core.auth_matrix import load_auth_matrix, merge_auth_headers
232
+
233
+ cases = load_auth_matrix(args.auth_matrix)
234
+ for c in cases:
235
+ req2 = merge_auth_headers(request, c)
236
+ report_format = args.report_format
237
+ if args.fail_on_regression and report_format != "json":
238
+ # regression gate needs a machine-readable report
239
+ report_format = "json"
240
+
241
+ decision, results, report_path = orch.run(
242
+ req2,
243
+ report_format=report_format,
244
+ gate_warn=args.gate_warn,
245
+ gate_block=args.gate_block,
246
+ run_label=f"auth-{c.name}",
247
+ fail_on_regression=args.fail_on_regression,
248
+ return_report_path=True,
249
+ )
250
+ _print_console(decision, results, label=c.name)
251
+
252
+ if args.fail_on_regression:
253
+ _maybe_fail_on_regression(report_path)
254
+ else:
255
+ report_format = args.report_format
256
+ if args.fail_on_regression and report_format != "json":
257
+ report_format = "json"
258
+
259
+ decision, results, report_path = orch.run(
260
+ request,
261
+ report_format=report_format,
262
+ gate_warn=args.gate_warn,
263
+ gate_block=args.gate_block,
264
+ fail_on_regression=args.fail_on_regression,
265
+ return_report_path=True,
166
266
  )
267
+ _print_console(decision, results)
268
+
269
+ if args.fail_on_regression:
270
+ _maybe_fail_on_regression(report_path)
167
271
 
168
272
 
169
273
  if __name__ == "__main__":
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class AuthCase:
12
+ name: str
13
+ headers: dict[str, str]
14
+
15
+
16
+ def _sanitize_case_name(name: str) -> str:
17
+ name = str(name or "").strip()
18
+ if not name:
19
+ return "case"
20
+ # Keep it filesystem-friendly.
21
+ out = []
22
+ for ch in name:
23
+ if ch.isalnum() or ch in ("-", "_", "."):
24
+ out.append(ch)
25
+ else:
26
+ out.append("-")
27
+ return "".join(out).strip("-") or "case"
28
+
29
+
30
+ def load_auth_matrix(path: str | Path) -> list[AuthCase]:
31
+ """Load an auth matrix config (yaml/json).
32
+
33
+ Schema:
34
+ {
35
+ "cases": [
36
+ {"name": "user", "headers": {"Authorization": "Bearer ..."}},
37
+ {"name": "admin", "headers": {"Authorization": "Bearer ..."}}
38
+ ]
39
+ }
40
+
41
+ Notes:
42
+ - This is intentionally minimal and explicit.
43
+ - Headers are merged into the base request headers (case wins).
44
+ """
45
+
46
+ p = Path(path)
47
+ raw = p.read_text(encoding="utf-8")
48
+ if p.suffix.lower() in {".yaml", ".yml"}:
49
+ data = yaml.safe_load(raw) or {}
50
+ else:
51
+ data = json.loads(raw)
52
+
53
+ cases = data.get("cases") if isinstance(data, dict) else None
54
+ if not isinstance(cases, list) or not cases:
55
+ raise ValueError("auth matrix must contain a non-empty 'cases' list")
56
+
57
+ out: list[AuthCase] = []
58
+ for i, c in enumerate(cases):
59
+ if not isinstance(c, dict):
60
+ raise ValueError(f"auth case #{i} must be an object")
61
+ name = _sanitize_case_name(c.get("name") or f"case{i+1}")
62
+ headers = c.get("headers") or {}
63
+ if not isinstance(headers, dict):
64
+ raise ValueError(f"auth case '{name}' headers must be an object")
65
+ # stringify values (avoid accidental ints)
66
+ headers2 = {str(k): str(v) for k, v in headers.items() if v is not None}
67
+ out.append(AuthCase(name=name, headers=headers2))
68
+
69
+ # Ensure unique names
70
+ seen: set[str] = set()
71
+ uniq: list[AuthCase] = []
72
+ for c in out:
73
+ nm = c.name
74
+ if nm not in seen:
75
+ uniq.append(c)
76
+ seen.add(nm)
77
+ else:
78
+ j = 2
79
+ while f"{nm}-{j}" in seen:
80
+ j += 1
81
+ new = f"{nm}-{j}"
82
+ uniq.append(AuthCase(name=new, headers=c.headers))
83
+ seen.add(new)
84
+
85
+ return uniq
86
+
87
+
88
+ def merge_auth_headers(request: dict, auth_case: AuthCase) -> dict:
89
+ req = dict(request)
90
+ base_headers = dict(req.get("headers") or {})
91
+ base_headers.update(auth_case.headers or {})
92
+ req["headers"] = base_headers
93
+ return req
@@ -20,3 +20,9 @@ AI_SWARM_USE_OPENAI = os.getenv("AI_SWARM_USE_OPENAI", "0").lower() in {"1", "tr
20
20
  AI_SWARM_OPENAI_MODEL = os.getenv("AI_SWARM_OPENAI_MODEL", "gpt-4.1-mini")
21
21
  AI_SWARM_MAX_AI_TESTS = env_int("AI_SWARM_MAX_AI_TESTS", 20)
22
22
  AI_SWARM_AI_SUMMARY = os.getenv("AI_SWARM_AI_SUMMARY", "0").lower() in {"1", "true", "yes"}
23
+
24
+ # OpenAPI schema-based request-body fuzzing (on by default when OpenAPI context exists)
25
+ AI_SWARM_OPENAPI_FUZZ = os.getenv("AI_SWARM_OPENAPI_FUZZ", "1").lower() in {"1", "true", "yes"}
26
+ AI_SWARM_OPENAPI_FUZZ_MAX_VALID = env_int("AI_SWARM_OPENAPI_FUZZ_MAX_VALID", 8)
27
+ AI_SWARM_OPENAPI_FUZZ_MAX_INVALID = env_int("AI_SWARM_OPENAPI_FUZZ_MAX_INVALID", 8)
28
+ AI_SWARM_OPENAPI_FUZZ_MAX_DEPTH = env_int("AI_SWARM_OPENAPI_FUZZ_MAX_DEPTH", 6)