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.
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/PKG-INFO +47 -2
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/README.md +44 -1
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/pyproject.toml +4 -1
- ai_testing_swarm-0.1.17/src/ai_testing_swarm/__init__.py +1 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/test_planner_agent.py +52 -1
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/cli.py +126 -22
- ai_testing_swarm-0.1.17/src/ai_testing_swarm/core/auth_matrix.py +93 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/config.py +6 -0
- ai_testing_swarm-0.1.17/src/ai_testing_swarm/core/openapi_fuzzer.py +396 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/risk.py +22 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/orchestrator.py +15 -1
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/reporting/dashboard.py +35 -5
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/reporting/report_writer.py +73 -0
- ai_testing_swarm-0.1.17/src/ai_testing_swarm/reporting/trend.py +110 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/PKG-INFO +47 -2
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/SOURCES.txt +5 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/requires.txt +3 -0
- ai_testing_swarm-0.1.17/tests/test_batch2_trend_and_auth.py +73 -0
- ai_testing_swarm-0.1.17/tests/test_openapi_fuzzer.py +91 -0
- ai_testing_swarm-0.1.15/src/ai_testing_swarm/__init__.py +0 -1
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/setup.cfg +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/__init__.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/execution_agent.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/learning_agent.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/llm_reasoning_agent.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/release_gate_agent.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/test_writer_agent.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/agents/ui_agent.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/__init__.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/api_client.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/curl_parser.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/openai_client.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/openapi_loader.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/openapi_validator.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/core/safety.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm/reporting/__init__.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/dependency_links.txt +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/entry_points.txt +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/src/ai_testing_swarm.egg-info/top_level.txt +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/tests/test_openapi_loader.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/tests/test_openapi_validator.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/tests/test_policy_expected_negatives.py +0 -0
- {ai_testing_swarm-0.1.15 → ai_testing_swarm-0.1.17}/tests/test_risk_scoring_and_gate.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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)
|