ai-testing-swarm 0.1.13__tar.gz → 0.1.15__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 (40) hide show
  1. {ai_testing_swarm-0.1.13/src/ai_testing_swarm.egg-info → ai_testing_swarm-0.1.15}/PKG-INFO +22 -4
  2. ai_testing_swarm-0.1.13/PKG-INFO → ai_testing_swarm-0.1.15/README.md +17 -12
  3. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/pyproject.toml +11 -1
  4. ai_testing_swarm-0.1.15/src/ai_testing_swarm/__init__.py +1 -0
  5. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/agents/execution_agent.py +23 -0
  6. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/cli.py +36 -2
  7. ai_testing_swarm-0.1.15/src/ai_testing_swarm/core/openapi_validator.py +157 -0
  8. ai_testing_swarm-0.1.15/src/ai_testing_swarm/core/risk.py +139 -0
  9. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/orchestrator.py +48 -8
  10. ai_testing_swarm-0.1.15/src/ai_testing_swarm/reporting/dashboard.py +188 -0
  11. ai_testing_swarm-0.1.15/src/ai_testing_swarm/reporting/report_writer.py +374 -0
  12. ai_testing_swarm-0.1.13/README.md → ai_testing_swarm-0.1.15/src/ai_testing_swarm.egg-info/PKG-INFO +30 -3
  13. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm.egg-info/SOURCES.txt +6 -0
  14. ai_testing_swarm-0.1.15/src/ai_testing_swarm.egg-info/requires.txt +5 -0
  15. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/tests/test_openapi_loader.py +1 -0
  16. ai_testing_swarm-0.1.15/tests/test_openapi_validator.py +77 -0
  17. ai_testing_swarm-0.1.15/tests/test_risk_scoring_and_gate.py +41 -0
  18. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/tests/test_swarm_api.py +8 -5
  19. ai_testing_swarm-0.1.13/src/ai_testing_swarm/__init__.py +0 -1
  20. ai_testing_swarm-0.1.13/src/ai_testing_swarm/reporting/report_writer.py +0 -147
  21. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/setup.cfg +0 -0
  22. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/agents/__init__.py +0 -0
  23. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/agents/learning_agent.py +0 -0
  24. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/agents/llm_reasoning_agent.py +0 -0
  25. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/agents/release_gate_agent.py +0 -0
  26. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/agents/test_planner_agent.py +0 -0
  27. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/agents/test_writer_agent.py +0 -0
  28. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/agents/ui_agent.py +0 -0
  29. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/core/__init__.py +0 -0
  30. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/core/api_client.py +0 -0
  31. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/core/config.py +0 -0
  32. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/core/curl_parser.py +0 -0
  33. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/core/openai_client.py +0 -0
  34. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/core/openapi_loader.py +0 -0
  35. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/core/safety.py +0 -0
  36. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm/reporting/__init__.py +0 -0
  37. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm.egg-info/dependency_links.txt +0 -0
  38. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm.egg-info/entry_points.txt +0 -0
  39. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/src/ai_testing_swarm.egg-info/top_level.txt +0 -0
  40. {ai_testing_swarm-0.1.13 → ai_testing_swarm-0.1.15}/tests/test_policy_expected_negatives.py +0 -0
@@ -1,17 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-testing-swarm
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: AI-powered testing swarm
5
5
  Author-email: Arif Shah <ashah7775@gmail.com>
6
6
  License: MIT
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
+ Requires-Dist: requests>=2.28
10
+ Requires-Dist: PyYAML>=6.0
11
+ Provides-Extra: openapi
12
+ Requires-Dist: jsonschema>=4.0; extra == "openapi"
9
13
 
10
14
  # AI Testing Swarm
11
15
 
12
16
  AI Testing Swarm is a **super-advanced, mutation-driven API testing framework** (with optional OpenAPI + OpenAI augmentation) built on top of **pytest**.
13
17
 
14
- It generates a large set of deterministic negative/edge/security test cases for an API request, executes them (optionally in parallel, with retries/throttling), and produces a JSON report with summaries.
18
+ It generates a large set of deterministic negative/edge/security test cases for an API request, executes them (optionally in parallel, with retries/throttling), and produces a report (JSON/Markdown/HTML) with summaries.
15
19
 
16
20
  > Notes:
17
21
  > - UI testing is not the focus of the current releases.
@@ -25,6 +29,12 @@ It generates a large set of deterministic negative/edge/security test cases for
25
29
  pip install ai-testing-swarm
26
30
  ```
27
31
 
32
+ Optional (OpenAPI JSON schema validation for responses):
33
+
34
+ ```bash
35
+ pip install "ai-testing-swarm[openapi]"
36
+ ```
37
+
28
38
  CLI entrypoint:
29
39
 
30
40
  ```bash
@@ -49,9 +59,15 @@ Run:
49
59
  ai-test --input request.json
50
60
  ```
51
61
 
52
- A JSON report is written under:
62
+ Choose a report format:
63
+
64
+ ```bash
65
+ ai-test --input request.json --report-format html
66
+ ```
67
+
68
+ A report is written under:
53
69
 
54
- - `./ai_swarm_reports/<METHOD>_<endpoint>/<METHOD>_<endpoint>_<timestamp>.json`
70
+ - `./ai_swarm_reports/<METHOD>_<endpoint>/<METHOD>_<endpoint>_<timestamp>.<json|md|html>`
55
71
 
56
72
  Reports include:
57
73
  - per-test results
@@ -97,6 +113,8 @@ Reports include:
97
113
  - OpenAPI **JSON** works by default.
98
114
  - OpenAPI **YAML** requires `PyYAML` installed.
99
115
  - Base URL is read from `spec.servers[0].url`.
116
+ - When using OpenAPI input, the swarm will also *optionally* validate response status codes against `operation.responses`.
117
+ - If `jsonschema` is installed (via `ai-testing-swarm[openapi]`) and the response is JSON, response bodies are validated against the OpenAPI `application/json` schema.
100
118
  - Override with `AI_SWARM_OPENAPI_BASE_URL` if your spec doesn’t include servers.
101
119
 
102
120
  ---
@@ -1,17 +1,8 @@
1
- Metadata-Version: 2.4
2
- Name: ai-testing-swarm
3
- Version: 0.1.13
4
- Summary: AI-powered testing swarm
5
- Author-email: Arif Shah <ashah7775@gmail.com>
6
- License: MIT
7
- Requires-Python: >=3.9
8
- Description-Content-Type: text/markdown
9
-
10
1
  # AI Testing Swarm
11
2
 
12
3
  AI Testing Swarm is a **super-advanced, mutation-driven API testing framework** (with optional OpenAPI + OpenAI augmentation) built on top of **pytest**.
13
4
 
14
- It generates a large set of deterministic negative/edge/security test cases for an API request, executes them (optionally in parallel, with retries/throttling), and produces a JSON report with summaries.
5
+ It generates a large set of deterministic negative/edge/security test cases for an API request, executes them (optionally in parallel, with retries/throttling), and produces a report (JSON/Markdown/HTML) with summaries.
15
6
 
16
7
  > Notes:
17
8
  > - UI testing is not the focus of the current releases.
@@ -25,6 +16,12 @@ It generates a large set of deterministic negative/edge/security test cases for
25
16
  pip install ai-testing-swarm
26
17
  ```
27
18
 
19
+ Optional (OpenAPI JSON schema validation for responses):
20
+
21
+ ```bash
22
+ pip install "ai-testing-swarm[openapi]"
23
+ ```
24
+
28
25
  CLI entrypoint:
29
26
 
30
27
  ```bash
@@ -49,9 +46,15 @@ Run:
49
46
  ai-test --input request.json
50
47
  ```
51
48
 
52
- A JSON report is written under:
49
+ Choose a report format:
50
+
51
+ ```bash
52
+ ai-test --input request.json --report-format html
53
+ ```
54
+
55
+ A report is written under:
53
56
 
54
- - `./ai_swarm_reports/<METHOD>_<endpoint>/<METHOD>_<endpoint>_<timestamp>.json`
57
+ - `./ai_swarm_reports/<METHOD>_<endpoint>/<METHOD>_<endpoint>_<timestamp>.<json|md|html>`
55
58
 
56
59
  Reports include:
57
60
  - per-test results
@@ -97,6 +100,8 @@ Reports include:
97
100
  - OpenAPI **JSON** works by default.
98
101
  - OpenAPI **YAML** requires `PyYAML` installed.
99
102
  - Base URL is read from `spec.servers[0].url`.
103
+ - When using OpenAPI input, the swarm will also *optionally* validate response status codes against `operation.responses`.
104
+ - If `jsonschema` is installed (via `ai-testing-swarm[openapi]`) and the response is JSON, response bodies are validated against the OpenAPI `application/json` schema.
100
105
  - Override with `AI_SWARM_OPENAPI_BASE_URL` if your spec doesn’t include servers.
101
106
 
102
107
  ---
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-testing-swarm"
7
- version = "0.1.13"
7
+ version = "0.1.15"
8
8
  description = "AI-powered testing swarm"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -13,6 +13,16 @@ authors = [
13
13
  ]
14
14
  license = { text = "MIT" }
15
15
 
16
+ dependencies = [
17
+ "requests>=2.28",
18
+ "PyYAML>=6.0",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ openapi = [
23
+ "jsonschema>=4.0",
24
+ ]
25
+
16
26
  [project.scripts]
17
27
  ai-test = "ai_testing_swarm.cli:main"
18
28
 
@@ -0,0 +1 @@
1
+ __version__ = "0.1.15"
@@ -137,6 +137,27 @@ class ExecutionAgent:
137
137
  except Exception:
138
138
  body_snippet = resp.text
139
139
 
140
+ openapi_validation = []
141
+ try:
142
+ ctx = request.get("_openapi") or {}
143
+ if isinstance(ctx, dict) and ctx.get("spec") and ctx.get("path") and ctx.get("method"):
144
+ # If method was mutated to something else, we can't reliably map to an OpenAPI op.
145
+ if str(ctx.get("method")).upper() == str(method).upper():
146
+ from ai_testing_swarm.core.openapi_validator import validate_openapi_response
147
+
148
+ issues = validate_openapi_response(
149
+ spec=ctx["spec"],
150
+ path=ctx["path"],
151
+ method=ctx["method"],
152
+ status_code=resp.status_code,
153
+ response_headers=dict(resp.headers),
154
+ response_json=body_snippet if isinstance(body_snippet, (dict, list)) else None,
155
+ )
156
+ openapi_validation = [i.__dict__ for i in issues]
157
+ except Exception:
158
+ # Best-effort only; never fail execution on validator issues.
159
+ openapi_validation = []
160
+
140
161
  return {
141
162
  "name": test["name"],
142
163
  "mutation": mutation,
@@ -151,7 +172,9 @@ class ExecutionAgent:
151
172
  "status_code": resp.status_code,
152
173
  "elapsed_ms": elapsed_ms,
153
174
  "attempt": attempt,
175
+ "headers": dict(resp.headers),
154
176
  "body_snippet": body_snippet,
177
+ "openapi_validation": openapi_validation,
155
178
  },
156
179
  }
157
180
 
@@ -31,7 +31,7 @@ def normalize_request(payload: dict) -> dict:
31
31
  from ai_testing_swarm.core.openapi_loader import load_openapi, build_request_from_openapi
32
32
 
33
33
  spec = load_openapi(payload["openapi"])
34
- return build_request_from_openapi(
34
+ req = build_request_from_openapi(
35
35
  spec,
36
36
  path=payload["path"],
37
37
  method=payload["method"],
@@ -40,6 +40,14 @@ def normalize_request(payload: dict) -> dict:
40
40
  query_params=payload.get("query_params") or {},
41
41
  body=payload.get("body"),
42
42
  )
43
+ # Attach OpenAPI context for optional response validation/reporting.
44
+ req["_openapi"] = {
45
+ "source": payload["openapi"],
46
+ "path": payload["path"],
47
+ "method": str(payload["method"]).upper(),
48
+ "spec": spec,
49
+ }
50
+ return req
43
51
 
44
52
  # Case 2: already normalized
45
53
  required_keys = {"method", "url"}
@@ -92,6 +100,27 @@ def main():
92
100
  help="Safety: allow only public test hosts (httpbin/postman-echo/reqres) for this run",
93
101
  )
94
102
 
103
+ parser.add_argument(
104
+ "--report-format",
105
+ default="json",
106
+ choices=["json", "md", "html"],
107
+ help="Report format to write (default: json)",
108
+ )
109
+
110
+ # Batch1: risk gate thresholds (backward compatible defaults)
111
+ parser.add_argument(
112
+ "--gate-warn",
113
+ type=int,
114
+ default=30,
115
+ help="Gate WARN threshold for endpoint risk score (default: 30)",
116
+ )
117
+ parser.add_argument(
118
+ "--gate-block",
119
+ type=int,
120
+ default=80,
121
+ help="Gate BLOCK threshold for endpoint risk score (default: 80)",
122
+ )
123
+
95
124
  args = parser.parse_args()
96
125
 
97
126
  # ------------------------------------------------------------
@@ -112,7 +141,12 @@ def main():
112
141
  import os
113
142
  os.environ["AI_SWARM_PUBLIC_ONLY"] = "1"
114
143
 
115
- decision, results = SwarmOrchestrator().run(request)
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
+ )
116
150
 
117
151
  # ------------------------------------------------------------
118
152
  # Console output
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class OpenAPIValidationIssue:
8
+ type: str
9
+ message: str
10
+ details: dict | None = None
11
+
12
+
13
+ def _matches_status_key(status_code: int, key: str) -> bool:
14
+ """OpenAPI response keys can be explicit ("200"), wildcard ("2XX"), or "default"."""
15
+ key = str(key).strip().upper()
16
+ if key == "DEFAULT":
17
+ return True
18
+ if len(key) == 3 and key.endswith("XX") and key[0].isdigit():
19
+ return int(key[0]) == int(status_code / 100)
20
+ return key == str(status_code)
21
+
22
+
23
+ def _get_operation(spec: dict, *, path: str, method: str) -> dict | None:
24
+ paths = spec.get("paths") or {}
25
+ op = (paths.get(path) or {}).get(str(method).lower())
26
+ if isinstance(op, dict):
27
+ return op
28
+ return None
29
+
30
+
31
+ def validate_openapi_response(
32
+ *,
33
+ spec: dict,
34
+ path: str,
35
+ method: str,
36
+ status_code: int | None,
37
+ response_headers: dict | None = None,
38
+ response_json=None,
39
+ ) -> list[OpenAPIValidationIssue]:
40
+ """Validate a response against an OpenAPI operation.
41
+
42
+ - Status code must be declared in operation.responses (supports 2XX and default)
43
+ - If response appears to be JSON and jsonschema is installed, validate body
44
+
45
+ Returns a list of issues (empty => OK or validation skipped).
46
+ """
47
+
48
+ issues: list[OpenAPIValidationIssue] = []
49
+
50
+ op = _get_operation(spec, path=path, method=method)
51
+ if not op:
52
+ return [
53
+ OpenAPIValidationIssue(
54
+ type="openapi_operation_missing",
55
+ message=f"Operation not found in spec: {str(method).upper()} {path}",
56
+ )
57
+ ]
58
+
59
+ responses = op.get("responses") or {}
60
+ if status_code is None:
61
+ # Network errors etc. Not an OpenAPI mismatch.
62
+ return issues
63
+
64
+ # --------------------
65
+ # Status validation
66
+ # --------------------
67
+ declared_keys = [str(k) for k in responses.keys()]
68
+ if declared_keys:
69
+ if not any(_matches_status_key(int(status_code), k) for k in declared_keys):
70
+ issues.append(
71
+ OpenAPIValidationIssue(
72
+ type="openapi_status",
73
+ message=f"Status {status_code} not declared in OpenAPI responses: {declared_keys}",
74
+ details={"status_code": status_code, "declared": declared_keys},
75
+ )
76
+ )
77
+
78
+ # --------------------
79
+ # JSON schema validation (optional)
80
+ # --------------------
81
+ ct = ""
82
+ if response_headers and isinstance(response_headers, dict):
83
+ for k, v in response_headers.items():
84
+ if str(k).lower() == "content-type" and v:
85
+ ct = str(v).lower()
86
+ break
87
+
88
+ is_json = isinstance(response_json, (dict, list))
89
+ if not is_json:
90
+ # if body wasn't parsed as JSON, skip schema validation
91
+ return issues
92
+
93
+ if ct and "json" not in ct:
94
+ # Respect explicit non-JSON content-type
95
+ return issues
96
+
97
+ # Find best matching response schema: exact > 2XX > default
98
+ chosen_resp: dict | None = None
99
+ for k in (str(status_code), f"{int(status_code/100)}XX", "default"):
100
+ if k in responses:
101
+ chosen_resp = responses.get(k)
102
+ break
103
+
104
+ if not isinstance(chosen_resp, dict):
105
+ return issues
106
+
107
+ content = chosen_resp.get("content") or {}
108
+ schema = None
109
+ if isinstance(content, dict):
110
+ app_json = content.get("application/json") or content.get("application/*+json")
111
+ if isinstance(app_json, dict):
112
+ schema = app_json.get("schema")
113
+
114
+ if not schema:
115
+ return issues
116
+
117
+ try:
118
+ import jsonschema # type: ignore
119
+
120
+ # Resolve in-document refs like #/components/schemas/X
121
+ resolver = jsonschema.RefResolver.from_schema(spec) # type: ignore[attr-defined]
122
+ validator_cls = jsonschema.validators.validator_for(schema) # type: ignore[attr-defined]
123
+ validator_cls.check_schema(schema)
124
+ validator = validator_cls(schema, resolver=resolver)
125
+ errors = sorted(validator.iter_errors(response_json), key=lambda e: list(e.path))
126
+ if errors:
127
+ # Keep just the first few errors for signal.
128
+ details = {
129
+ "error_count": len(errors),
130
+ "errors": [
131
+ {
132
+ "message": e.message,
133
+ "path": list(e.path),
134
+ "schema_path": list(e.schema_path),
135
+ }
136
+ for e in errors[:5]
137
+ ],
138
+ }
139
+ issues.append(
140
+ OpenAPIValidationIssue(
141
+ type="openapi_schema",
142
+ message="Response JSON does not match OpenAPI schema",
143
+ details=details,
144
+ )
145
+ )
146
+ except ImportError:
147
+ # Optional dependency not installed: status validation still works.
148
+ return issues
149
+ except Exception as e:
150
+ issues.append(
151
+ OpenAPIValidationIssue(
152
+ type="openapi_schema_error",
153
+ message=f"OpenAPI schema validation failed: {e}",
154
+ )
155
+ )
156
+
157
+ return issues
@@ -0,0 +1,139 @@
1
+ """Risk scoring for AI Testing Swarm.
2
+
3
+ Batch1 additions:
4
+ - Compute a numeric risk_score per test result (0..100)
5
+ - Aggregate endpoint risk and drive gate thresholds (PASS/WARN/BLOCK)
6
+
7
+ Design goals:
8
+ - Backward compatible: consumers can ignore risk_score fields.
9
+ - Deterministic and explainable.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class RiskThresholds:
19
+ """Thresholds for the endpoint gate.
20
+
21
+ Gate is computed from endpoint_risk_score (currently max test risk).
22
+
23
+ - PASS: score < warn
24
+ - WARN: warn <= score < block
25
+ - BLOCK: score >= block
26
+ """
27
+
28
+ warn: int = 30
29
+ block: int = 80
30
+
31
+
32
+ # Keep aligned with orchestrator/release gate semantics
33
+ EXPECTED_FAILURES: set[str] = {
34
+ "success",
35
+ "missing_param",
36
+ "invalid_param",
37
+ "security",
38
+ "method_not_allowed",
39
+ "not_found",
40
+ "content_negotiation",
41
+ }
42
+
43
+ RISKY_FAILURES: set[str] = {
44
+ "unknown",
45
+ "missing_param_accepted",
46
+ "null_param_accepted",
47
+ "invalid_param_accepted",
48
+ "headers_accepted",
49
+ "method_risk",
50
+ }
51
+
52
+ BLOCKING_FAILURES: set[str] = {
53
+ "auth_issue",
54
+ "infra",
55
+ "security_risk",
56
+ "server_error",
57
+ }
58
+
59
+
60
+ def clamp_int(x: int, lo: int, hi: int) -> int:
61
+ return lo if x < lo else hi if x > hi else x
62
+
63
+
64
+ def compute_test_risk_score(result: dict, *, sla_ms: int | None = None) -> int:
65
+ """Compute a risk score for a single test result.
66
+
67
+ Inputs expected (best-effort):
68
+ - result['failure_type']
69
+ - result['status']
70
+ - result['response']['status_code']
71
+ - result['response']['elapsed_ms']
72
+ - result['response']['openapi_validation'] (list)
73
+
74
+ Returns: int in range 0..100.
75
+ """
76
+
77
+ ft = str(result.get("failure_type") or "unknown")
78
+ status = str(result.get("status") or "")
79
+ resp = result.get("response") or {}
80
+ sc = resp.get("status_code")
81
+
82
+ # Base score from semantic classification.
83
+ if ft in EXPECTED_FAILURES:
84
+ base = 0
85
+ elif ft in RISKY_FAILURES:
86
+ base = 35
87
+ elif ft in BLOCKING_FAILURES:
88
+ base = 90
89
+ else:
90
+ # Unknown failure types are treated as high risk but not always a hard blocker.
91
+ base = 60
92
+
93
+ # Status-code adjustments (defense in depth)
94
+ if isinstance(sc, int):
95
+ if 500 <= sc:
96
+ base = max(base, 90)
97
+ elif sc in (401, 403):
98
+ base = max(base, 80)
99
+ elif 400 <= sc < 500:
100
+ # Client errors for negative tests are expected; keep base.
101
+ base = max(base, base)
102
+
103
+ # Explicit FAILED status implies something unexpected.
104
+ if status.upper() == "FAILED":
105
+ base = max(base, 70)
106
+
107
+ # OpenAPI validation issues are a small additive risk.
108
+ issues = resp.get("openapi_validation") or []
109
+ if isinstance(issues, list) and issues:
110
+ base += 10
111
+
112
+ # SLA breach is a small additive risk.
113
+ if sla_ms is not None:
114
+ elapsed = resp.get("elapsed_ms")
115
+ if isinstance(elapsed, int) and elapsed > sla_ms:
116
+ base += 5
117
+
118
+ return clamp_int(int(base), 0, 100)
119
+
120
+
121
+ def compute_endpoint_risk_score(results: list[dict]) -> int:
122
+ """Aggregate endpoint risk score.
123
+
124
+ Current policy: endpoint risk is the max risk_score across tests.
125
+ (This makes gating stable and easy to interpret.)
126
+ """
127
+
128
+ scores = [r.get("risk_score") for r in (results or []) if isinstance(r.get("risk_score"), int)]
129
+ if not scores:
130
+ return 0
131
+ return int(max(scores))
132
+
133
+
134
+ def gate_from_score(score: int, thresholds: RiskThresholds) -> str:
135
+ if score >= thresholds.block:
136
+ return "BLOCK"
137
+ if score >= thresholds.warn:
138
+ return "WARN"
139
+ return "PASS"
@@ -5,6 +5,12 @@ from ai_testing_swarm.agents.learning_agent import LearningAgent
5
5
  from ai_testing_swarm.agents.release_gate_agent import ReleaseGateAgent
6
6
  from ai_testing_swarm.reporting.report_writer import write_report
7
7
  from ai_testing_swarm.core.safety import enforce_public_only
8
+ from ai_testing_swarm.core.risk import (
9
+ RiskThresholds,
10
+ compute_test_risk_score,
11
+ compute_endpoint_risk_score,
12
+ gate_from_score,
13
+ )
8
14
 
9
15
  import logging
10
16
 
@@ -40,8 +46,20 @@ class SwarmOrchestrator:
40
46
  self.learner = LearningAgent()
41
47
  self.release_gate = ReleaseGateAgent()
42
48
 
43
- def run(self, request: dict):
44
- """Runs the full AI testing swarm and returns (decision, results)."""
49
+ def run(
50
+ self,
51
+ request: dict,
52
+ *,
53
+ report_format: str = "json",
54
+ gate_warn: int = 30,
55
+ gate_block: int = 80,
56
+ ):
57
+ """Runs the full AI testing swarm and returns (decision, results).
58
+
59
+ gate_warn/gate_block:
60
+ Thresholds for PASS/WARN/BLOCK gate based on endpoint risk.
61
+ (Kept optional for backward compatibility.)
62
+ """
45
63
 
46
64
  # Safety hook (currently no-op; kept for backward compatibility)
47
65
  enforce_public_only(request["url"])
@@ -78,6 +96,19 @@ class SwarmOrchestrator:
78
96
  else "FAILED"
79
97
  ),
80
98
  })
99
+
100
+ # Batch1: numeric risk score per test (0..100)
101
+ try:
102
+ from ai_testing_swarm.core.config import AI_SWARM_SLA_MS
103
+
104
+ execution_result["risk_score"] = compute_test_risk_score(
105
+ execution_result,
106
+ sla_ms=AI_SWARM_SLA_MS,
107
+ )
108
+ except Exception:
109
+ # Keep backwards compatibility: don't fail the run if scoring breaks.
110
+ execution_result["risk_score"] = 0
111
+
81
112
  # Optional learning step
82
113
  try:
83
114
  self.learner.learn(test_name, classification)
@@ -97,14 +128,23 @@ class SwarmOrchestrator:
97
128
  results.append(results_by_name[t["name"]])
98
129
 
99
130
  # --------------------------------------------------------
100
- # 3️⃣ RELEASE DECISION
131
+ # 3️⃣ RELEASE DECISION + RISK GATE
101
132
  # --------------------------------------------------------
102
133
  decision = self.release_gate.decide(results)
103
134
 
135
+ thresholds = RiskThresholds(warn=int(gate_warn), block=int(gate_block))
136
+ endpoint_risk_score = compute_endpoint_risk_score(results)
137
+ gate_status = gate_from_score(endpoint_risk_score, thresholds)
138
+
104
139
  # --------------------------------------------------------
105
- # 4️⃣ WRITE JSON REPORT
140
+ # 4️⃣ WRITE REPORT
106
141
  # --------------------------------------------------------
107
- meta = {"decision": decision}
142
+ meta = {
143
+ "decision": decision,
144
+ "gate_status": gate_status,
145
+ "gate_thresholds": {"warn": thresholds.warn, "block": thresholds.block},
146
+ "endpoint_risk_score": endpoint_risk_score,
147
+ }
108
148
 
109
149
  # Optional AI summary for humans (best-effort)
110
150
  try:
@@ -122,8 +162,8 @@ class SwarmOrchestrator:
122
162
  except Exception as e:
123
163
  meta["ai_summary_error"] = str(e)
124
164
 
125
- report_path = write_report(request, results, meta=meta)
126
- logger.info("📄 Swarm JSON report written to: %s", report_path)
127
- print(f"📄 Swarm JSON report written to: {report_path}")
165
+ report_path = write_report(request, results, meta=meta, report_format=report_format)
166
+ logger.info("📄 Swarm report written to: %s", report_path)
167
+ print(f"📄 Swarm report written to: {report_path}")
128
168
 
129
169
  return decision, results