ai-testing-swarm 0.1.1__tar.gz → 0.1.11__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.11/PKG-INFO +231 -0
- ai_testing_swarm-0.1.11/README.md +222 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/pyproject.toml +1 -1
- ai_testing_swarm-0.1.11/src/ai_testing_swarm/__init__.py +1 -0
- ai_testing_swarm-0.1.11/src/ai_testing_swarm/agents/execution_agent.py +186 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/agents/learning_agent.py +6 -4
- ai_testing_swarm-0.1.11/src/ai_testing_swarm/agents/test_planner_agent.py +396 -0
- ai_testing_swarm-0.1.11/src/ai_testing_swarm/cli.py +136 -0
- ai_testing_swarm-0.1.11/src/ai_testing_swarm/core/config.py +22 -0
- ai_testing_swarm-0.1.11/src/ai_testing_swarm/core/openai_client.py +182 -0
- ai_testing_swarm-0.1.11/src/ai_testing_swarm/core/openapi_loader.py +130 -0
- ai_testing_swarm-0.1.11/src/ai_testing_swarm/core/safety.py +32 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/orchestrator.py +50 -28
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/reporting/report_writer.py +61 -3
- ai_testing_swarm-0.1.11/src/ai_testing_swarm.egg-info/PKG-INFO +231 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm.egg-info/SOURCES.txt +5 -0
- ai_testing_swarm-0.1.11/tests/test_openapi_loader.py +18 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/tests/test_swarm_api.py +7 -1
- ai_testing_swarm-0.1.1/PKG-INFO +0 -55
- ai_testing_swarm-0.1.1/README.md +0 -46
- ai_testing_swarm-0.1.1/src/ai_testing_swarm/__init__.py +0 -1
- ai_testing_swarm-0.1.1/src/ai_testing_swarm/agents/execution_agent.py +0 -64
- ai_testing_swarm-0.1.1/src/ai_testing_swarm/agents/test_planner_agent.py +0 -111
- ai_testing_swarm-0.1.1/src/ai_testing_swarm/cli.py +0 -78
- ai_testing_swarm-0.1.1/src/ai_testing_swarm.egg-info/PKG-INFO +0 -55
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/setup.cfg +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/agents/__init__.py +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/agents/llm_reasoning_agent.py +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/agents/release_gate_agent.py +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/agents/test_writer_agent.py +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/agents/ui_agent.py +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/core/__init__.py +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/core/api_client.py +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/core/curl_parser.py +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/reporting/__init__.py +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm.egg-info/dependency_links.txt +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm.egg-info/entry_points.txt +0 -0
- {ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-testing-swarm
|
|
3
|
+
Version: 0.1.11
|
|
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
|
+
# AI Testing Swarm
|
|
11
|
+
|
|
12
|
+
AI Testing Swarm is a **super-advanced, mutation-driven API testing framework** (with optional OpenAPI + OpenAI augmentation) built on top of **pytest**.
|
|
13
|
+
|
|
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.
|
|
15
|
+
|
|
16
|
+
> Notes:
|
|
17
|
+
> - UI testing is not the focus of the current releases.
|
|
18
|
+
> - OpenAI features are **optional** and disabled by default.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install ai-testing-swarm
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
CLI entrypoint:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
ai-test --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick start (cURL input)
|
|
37
|
+
|
|
38
|
+
Create `request.json`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"curl": "curl --location https://postman-echo.com/post --header \"Content-Type: application/json\" --data \"{\\\"hello\\\":\\\"world\\\",\\\"count\\\":1}\""
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ai-test --input request.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A JSON report is written under:
|
|
53
|
+
|
|
54
|
+
- `./ai_swarm_reports/<METHOD>_<endpoint>/<METHOD>_<endpoint>_<timestamp>.json`
|
|
55
|
+
|
|
56
|
+
Reports include:
|
|
57
|
+
- per-test results
|
|
58
|
+
- summary counts by status code / failure type
|
|
59
|
+
- optional AI summary (if enabled)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Input formats
|
|
64
|
+
|
|
65
|
+
### 1) Raw cURL
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{ "curl": "curl ..." }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2) Normalized request
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"method": "POST",
|
|
76
|
+
"url": "https://example.com/api/login",
|
|
77
|
+
"headers": {"content-type": "application/json"},
|
|
78
|
+
"params": {"a": "b"},
|
|
79
|
+
"body": {"username": "u", "password": "p"}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3) OpenAPI-driven (optional)
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"openapi": "./openapi.json",
|
|
88
|
+
"path": "/pets",
|
|
89
|
+
"method": "get",
|
|
90
|
+
"headers": {"accept": "application/json"},
|
|
91
|
+
"path_params": {"petId": "123"},
|
|
92
|
+
"query_params": {"limit": 10},
|
|
93
|
+
"body": null
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- OpenAPI **JSON** works by default.
|
|
98
|
+
- OpenAPI **YAML** requires `PyYAML` installed.
|
|
99
|
+
- Base URL is read from `spec.servers[0].url`.
|
|
100
|
+
- Override with `AI_SWARM_OPENAPI_BASE_URL` if your spec doesn’t include servers.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## What test cases are generated?
|
|
105
|
+
|
|
106
|
+
The swarm always includes:
|
|
107
|
+
- **happy_path** (baseline)
|
|
108
|
+
|
|
109
|
+
Then generates broad coverage across:
|
|
110
|
+
- **Method misuse**: same path with wrong HTTP methods (GET/PUT/PATCH/DELETE etc.)
|
|
111
|
+
- **Headers**: missing/invalid `Content-Type`, accept variations, and other header tampering
|
|
112
|
+
- **Auth** (if `Authorization` header exists): missing/invalid token tests
|
|
113
|
+
- **Body/query mutations** (per field):
|
|
114
|
+
- missing / null / empty / whitespace
|
|
115
|
+
- type probes (int/bool/float/array/object)
|
|
116
|
+
- boundary inputs (very long strings, huge ints, negative values)
|
|
117
|
+
- unicode + special character payloads
|
|
118
|
+
- **Security payload probes** (per field): SQLi/XSS/path traversal/log4j patterns
|
|
119
|
+
- **Whole-body mutations**: null body, empty object, extra unexpected field
|
|
120
|
+
|
|
121
|
+
> Output is deterministic unless OpenAI augmentation is enabled.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Safety mode (recommended for CI/demos)
|
|
126
|
+
|
|
127
|
+
Mutation testing can be noisy and may accidentally stress a real environment.
|
|
128
|
+
To force safe demo runs only against public test hosts:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
ai-test --input request.json --public-only
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Or via env:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
export AI_SWARM_PUBLIC_ONLY=1
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Allowed hosts in public-only mode:
|
|
141
|
+
- `httpbin.org`
|
|
142
|
+
- `postman-echo.com`
|
|
143
|
+
- `reqres.in`
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Performance features
|
|
148
|
+
|
|
149
|
+
### Parallel execution
|
|
150
|
+
- Enabled by default via thread pool.
|
|
151
|
+
- Control with:
|
|
152
|
+
- `AI_SWARM_WORKERS` (default: `5`)
|
|
153
|
+
|
|
154
|
+
### Retry + backoff (flaky endpoints)
|
|
155
|
+
- Retries on transient errors and status codes (408/429/5xx etc.)
|
|
156
|
+
- Control with:
|
|
157
|
+
- `AI_SWARM_RETRY_COUNT` (default: `1`)
|
|
158
|
+
- `AI_SWARM_RETRY_BACKOFF_MS` (default: `250`)
|
|
159
|
+
|
|
160
|
+
### Throttling (RPS)
|
|
161
|
+
- Global throttle to avoid hammering a target:
|
|
162
|
+
- `AI_SWARM_RPS` (default: `0` = disabled)
|
|
163
|
+
|
|
164
|
+
### Max test cap
|
|
165
|
+
- Avoids accidental DoS / CI timeouts:
|
|
166
|
+
- `AI_SWARM_MAX_TESTS` (default: `80`)
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Reporting
|
|
171
|
+
|
|
172
|
+
Reports include:
|
|
173
|
+
- `summary.counts_by_failure_type`
|
|
174
|
+
- `summary.counts_by_status_code`
|
|
175
|
+
- `summary.slow_tests` (based on SLA)
|
|
176
|
+
|
|
177
|
+
SLA threshold:
|
|
178
|
+
- `AI_SWARM_SLA_MS` (default: `2000`)
|
|
179
|
+
|
|
180
|
+
Security:
|
|
181
|
+
- Sensitive headers are redacted in the report (Authorization/Cookie/api tokens etc.)
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Optional OpenAI augmentation (advanced)
|
|
186
|
+
|
|
187
|
+
### A) Generate additional test cases (planner augmentation)
|
|
188
|
+
Enable:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
export AI_SWARM_USE_OPENAI=1
|
|
192
|
+
export OPENAI_API_KEY=...
|
|
193
|
+
export AI_SWARM_MAX_AI_TESTS=30
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### B) Human-readable AI summary in report
|
|
197
|
+
Enable:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
export AI_SWARM_USE_OPENAI=1
|
|
201
|
+
export AI_SWARM_AI_SUMMARY=1
|
|
202
|
+
export OPENAI_API_KEY=...
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Model selection:
|
|
206
|
+
- `AI_SWARM_OPENAI_MODEL` (default: `gpt-4.1-mini`)
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## CLI help
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
ai-test --help
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Release decisions
|
|
219
|
+
|
|
220
|
+
The swarm produces a release decision:
|
|
221
|
+
- `APPROVE_RELEASE`
|
|
222
|
+
- `APPROVE_RELEASE_WITH_RISKS`
|
|
223
|
+
- `REJECT_RELEASE`
|
|
224
|
+
|
|
225
|
+
The decision is derived from deterministic rules (not an LLM).
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# AI Testing Swarm
|
|
2
|
+
|
|
3
|
+
AI Testing Swarm is a **super-advanced, mutation-driven API testing framework** (with optional OpenAPI + OpenAI augmentation) built on top of **pytest**.
|
|
4
|
+
|
|
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 JSON report with summaries.
|
|
6
|
+
|
|
7
|
+
> Notes:
|
|
8
|
+
> - UI testing is not the focus of the current releases.
|
|
9
|
+
> - OpenAI features are **optional** and disabled by default.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install ai-testing-swarm
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
CLI entrypoint:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
ai-test --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick start (cURL input)
|
|
28
|
+
|
|
29
|
+
Create `request.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"curl": "curl --location https://postman-echo.com/post --header \"Content-Type: application/json\" --data \"{\\\"hello\\\":\\\"world\\\",\\\"count\\\":1}\""
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
ai-test --input request.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A JSON report is written under:
|
|
44
|
+
|
|
45
|
+
- `./ai_swarm_reports/<METHOD>_<endpoint>/<METHOD>_<endpoint>_<timestamp>.json`
|
|
46
|
+
|
|
47
|
+
Reports include:
|
|
48
|
+
- per-test results
|
|
49
|
+
- summary counts by status code / failure type
|
|
50
|
+
- optional AI summary (if enabled)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Input formats
|
|
55
|
+
|
|
56
|
+
### 1) Raw cURL
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "curl": "curl ..." }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2) Normalized request
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"method": "POST",
|
|
67
|
+
"url": "https://example.com/api/login",
|
|
68
|
+
"headers": {"content-type": "application/json"},
|
|
69
|
+
"params": {"a": "b"},
|
|
70
|
+
"body": {"username": "u", "password": "p"}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3) OpenAPI-driven (optional)
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"openapi": "./openapi.json",
|
|
79
|
+
"path": "/pets",
|
|
80
|
+
"method": "get",
|
|
81
|
+
"headers": {"accept": "application/json"},
|
|
82
|
+
"path_params": {"petId": "123"},
|
|
83
|
+
"query_params": {"limit": 10},
|
|
84
|
+
"body": null
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- OpenAPI **JSON** works by default.
|
|
89
|
+
- OpenAPI **YAML** requires `PyYAML` installed.
|
|
90
|
+
- Base URL is read from `spec.servers[0].url`.
|
|
91
|
+
- Override with `AI_SWARM_OPENAPI_BASE_URL` if your spec doesn’t include servers.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## What test cases are generated?
|
|
96
|
+
|
|
97
|
+
The swarm always includes:
|
|
98
|
+
- **happy_path** (baseline)
|
|
99
|
+
|
|
100
|
+
Then generates broad coverage across:
|
|
101
|
+
- **Method misuse**: same path with wrong HTTP methods (GET/PUT/PATCH/DELETE etc.)
|
|
102
|
+
- **Headers**: missing/invalid `Content-Type`, accept variations, and other header tampering
|
|
103
|
+
- **Auth** (if `Authorization` header exists): missing/invalid token tests
|
|
104
|
+
- **Body/query mutations** (per field):
|
|
105
|
+
- missing / null / empty / whitespace
|
|
106
|
+
- type probes (int/bool/float/array/object)
|
|
107
|
+
- boundary inputs (very long strings, huge ints, negative values)
|
|
108
|
+
- unicode + special character payloads
|
|
109
|
+
- **Security payload probes** (per field): SQLi/XSS/path traversal/log4j patterns
|
|
110
|
+
- **Whole-body mutations**: null body, empty object, extra unexpected field
|
|
111
|
+
|
|
112
|
+
> Output is deterministic unless OpenAI augmentation is enabled.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Safety mode (recommended for CI/demos)
|
|
117
|
+
|
|
118
|
+
Mutation testing can be noisy and may accidentally stress a real environment.
|
|
119
|
+
To force safe demo runs only against public test hosts:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
ai-test --input request.json --public-only
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or via env:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
export AI_SWARM_PUBLIC_ONLY=1
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Allowed hosts in public-only mode:
|
|
132
|
+
- `httpbin.org`
|
|
133
|
+
- `postman-echo.com`
|
|
134
|
+
- `reqres.in`
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Performance features
|
|
139
|
+
|
|
140
|
+
### Parallel execution
|
|
141
|
+
- Enabled by default via thread pool.
|
|
142
|
+
- Control with:
|
|
143
|
+
- `AI_SWARM_WORKERS` (default: `5`)
|
|
144
|
+
|
|
145
|
+
### Retry + backoff (flaky endpoints)
|
|
146
|
+
- Retries on transient errors and status codes (408/429/5xx etc.)
|
|
147
|
+
- Control with:
|
|
148
|
+
- `AI_SWARM_RETRY_COUNT` (default: `1`)
|
|
149
|
+
- `AI_SWARM_RETRY_BACKOFF_MS` (default: `250`)
|
|
150
|
+
|
|
151
|
+
### Throttling (RPS)
|
|
152
|
+
- Global throttle to avoid hammering a target:
|
|
153
|
+
- `AI_SWARM_RPS` (default: `0` = disabled)
|
|
154
|
+
|
|
155
|
+
### Max test cap
|
|
156
|
+
- Avoids accidental DoS / CI timeouts:
|
|
157
|
+
- `AI_SWARM_MAX_TESTS` (default: `80`)
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Reporting
|
|
162
|
+
|
|
163
|
+
Reports include:
|
|
164
|
+
- `summary.counts_by_failure_type`
|
|
165
|
+
- `summary.counts_by_status_code`
|
|
166
|
+
- `summary.slow_tests` (based on SLA)
|
|
167
|
+
|
|
168
|
+
SLA threshold:
|
|
169
|
+
- `AI_SWARM_SLA_MS` (default: `2000`)
|
|
170
|
+
|
|
171
|
+
Security:
|
|
172
|
+
- Sensitive headers are redacted in the report (Authorization/Cookie/api tokens etc.)
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Optional OpenAI augmentation (advanced)
|
|
177
|
+
|
|
178
|
+
### A) Generate additional test cases (planner augmentation)
|
|
179
|
+
Enable:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
export AI_SWARM_USE_OPENAI=1
|
|
183
|
+
export OPENAI_API_KEY=...
|
|
184
|
+
export AI_SWARM_MAX_AI_TESTS=30
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### B) Human-readable AI summary in report
|
|
188
|
+
Enable:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
export AI_SWARM_USE_OPENAI=1
|
|
192
|
+
export AI_SWARM_AI_SUMMARY=1
|
|
193
|
+
export OPENAI_API_KEY=...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Model selection:
|
|
197
|
+
- `AI_SWARM_OPENAI_MODEL` (default: `gpt-4.1-mini`)
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## CLI help
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
ai-test --help
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Release decisions
|
|
210
|
+
|
|
211
|
+
The swarm produces a release decision:
|
|
212
|
+
- `APPROVE_RELEASE`
|
|
213
|
+
- `APPROVE_RELEASE_WITH_RISKS`
|
|
214
|
+
- `REJECT_RELEASE`
|
|
215
|
+
|
|
216
|
+
The decision is derived from deterministic rules (not an LLM).
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.11"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import time
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from ai_testing_swarm.core.config import (
|
|
8
|
+
AI_SWARM_RETRY_BACKOFF_MS,
|
|
9
|
+
AI_SWARM_RETRY_COUNT,
|
|
10
|
+
AI_SWARM_RPS,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExecutionAgent:
|
|
15
|
+
def _set_path(self, obj: dict, path: list[str], value, remove: bool = False):
|
|
16
|
+
"""Set/remove nested key in a dict. Creates intermediate dicts if needed."""
|
|
17
|
+
if not path:
|
|
18
|
+
return
|
|
19
|
+
cur = obj
|
|
20
|
+
for key in path[:-1]:
|
|
21
|
+
if key not in cur or not isinstance(cur[key], dict):
|
|
22
|
+
cur[key] = {}
|
|
23
|
+
cur = cur[key]
|
|
24
|
+
|
|
25
|
+
last = path[-1]
|
|
26
|
+
if remove:
|
|
27
|
+
cur.pop(last, None)
|
|
28
|
+
else:
|
|
29
|
+
cur[last] = value
|
|
30
|
+
|
|
31
|
+
def apply_mutation(self, data: dict, mutation: dict):
|
|
32
|
+
if not mutation:
|
|
33
|
+
return deepcopy(data)
|
|
34
|
+
|
|
35
|
+
data = deepcopy(data)
|
|
36
|
+
path = mutation.get("path") or []
|
|
37
|
+
op = mutation.get("operation")
|
|
38
|
+
|
|
39
|
+
if op == "REMOVE":
|
|
40
|
+
self._set_path(data, path, None, remove=True)
|
|
41
|
+
elif op == "REPLACE":
|
|
42
|
+
self._set_path(data, path, mutation.get("new_value"), remove=False)
|
|
43
|
+
|
|
44
|
+
return data
|
|
45
|
+
|
|
46
|
+
def _throttle(self):
|
|
47
|
+
# Optional global throttling to avoid hammering target services.
|
|
48
|
+
if AI_SWARM_RPS and AI_SWARM_RPS > 0:
|
|
49
|
+
time.sleep(1.0 / float(AI_SWARM_RPS))
|
|
50
|
+
|
|
51
|
+
def _should_retry(self, status_code: int | None, error: str | None) -> bool:
|
|
52
|
+
if error:
|
|
53
|
+
return True
|
|
54
|
+
if status_code in (408, 425, 429, 500, 502, 503, 504):
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def execute(self, request: dict, test: dict):
|
|
59
|
+
mutation = test.get("mutation")
|
|
60
|
+
|
|
61
|
+
# Method (allow method mutations)
|
|
62
|
+
method = request["method"].upper()
|
|
63
|
+
if mutation and mutation.get("target") == "method" and mutation.get("operation") == "REPLACE":
|
|
64
|
+
method = str(mutation.get("new_value") or method).upper()
|
|
65
|
+
|
|
66
|
+
params = (
|
|
67
|
+
self.apply_mutation(request.get("params", {}) or {}, mutation)
|
|
68
|
+
if mutation and mutation.get("target") == "query"
|
|
69
|
+
else deepcopy(request.get("params", {}) or {})
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
body = deepcopy(request.get("body", {}) or {})
|
|
73
|
+
if mutation and mutation.get("target") == "body":
|
|
74
|
+
body = self.apply_mutation(body, mutation)
|
|
75
|
+
elif mutation and mutation.get("target") == "body_whole" and mutation.get("operation") == "REPLACE":
|
|
76
|
+
body = mutation.get("new_value")
|
|
77
|
+
|
|
78
|
+
headers = deepcopy(request.get("headers", {}) or {})
|
|
79
|
+
if mutation and mutation.get("target") == "headers":
|
|
80
|
+
headers = self.apply_mutation(headers, mutation)
|
|
81
|
+
|
|
82
|
+
# Payload strategy (basic content-type aware)
|
|
83
|
+
send_json = method != "GET"
|
|
84
|
+
ct = (headers.get("content-type") or headers.get("Content-Type") or "").lower()
|
|
85
|
+
|
|
86
|
+
json_payload = None
|
|
87
|
+
data_payload = None
|
|
88
|
+
if method == "GET":
|
|
89
|
+
json_payload = None
|
|
90
|
+
data_payload = None
|
|
91
|
+
else:
|
|
92
|
+
# If body is not a dict, send raw data
|
|
93
|
+
if not isinstance(body, (dict, list)):
|
|
94
|
+
send_json = False
|
|
95
|
+
data_payload = body
|
|
96
|
+
elif "application/x-www-form-urlencoded" in ct:
|
|
97
|
+
send_json = False
|
|
98
|
+
data_payload = body
|
|
99
|
+
else:
|
|
100
|
+
json_payload = body
|
|
101
|
+
|
|
102
|
+
# Execute with light retry/backoff on transient failures.
|
|
103
|
+
attempt = 0
|
|
104
|
+
last_error = None
|
|
105
|
+
last_response = None
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
attempt += 1
|
|
109
|
+
self._throttle()
|
|
110
|
+
|
|
111
|
+
started = time.time()
|
|
112
|
+
try:
|
|
113
|
+
resp = requests.request(
|
|
114
|
+
method=method,
|
|
115
|
+
url=request["url"],
|
|
116
|
+
headers=headers,
|
|
117
|
+
params=params or None,
|
|
118
|
+
json=json_payload if send_json else None,
|
|
119
|
+
data=data_payload if not send_json else None,
|
|
120
|
+
timeout=15,
|
|
121
|
+
)
|
|
122
|
+
elapsed_ms = int((time.time() - started) * 1000)
|
|
123
|
+
|
|
124
|
+
last_error = None
|
|
125
|
+
last_response = (resp, elapsed_ms)
|
|
126
|
+
|
|
127
|
+
if attempt <= (AI_SWARM_RETRY_COUNT + 1) and self._should_retry(resp.status_code, None):
|
|
128
|
+
# jittered backoff
|
|
129
|
+
backoff = (AI_SWARM_RETRY_BACKOFF_MS / 1000.0) * (2 ** (attempt - 1))
|
|
130
|
+
backoff = backoff * (0.75 + random.random() * 0.5)
|
|
131
|
+
time.sleep(min(backoff, 5.0))
|
|
132
|
+
if attempt <= AI_SWARM_RETRY_COUNT:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
body_snippet = resp.json()
|
|
137
|
+
except Exception:
|
|
138
|
+
body_snippet = resp.text
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"name": test["name"],
|
|
142
|
+
"mutation": mutation,
|
|
143
|
+
"request": {
|
|
144
|
+
"method": method,
|
|
145
|
+
"url": request["url"],
|
|
146
|
+
"headers": headers,
|
|
147
|
+
"params": params,
|
|
148
|
+
"body": body if method != "GET" else None,
|
|
149
|
+
},
|
|
150
|
+
"response": {
|
|
151
|
+
"status_code": resp.status_code,
|
|
152
|
+
"elapsed_ms": elapsed_ms,
|
|
153
|
+
"attempt": attempt,
|
|
154
|
+
"body_snippet": body_snippet,
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
except requests.RequestException as e:
|
|
159
|
+
elapsed_ms = int((time.time() - started) * 1000)
|
|
160
|
+
last_error = str(e)
|
|
161
|
+
last_response = None
|
|
162
|
+
|
|
163
|
+
if attempt <= AI_SWARM_RETRY_COUNT and self._should_retry(None, last_error):
|
|
164
|
+
backoff = (AI_SWARM_RETRY_BACKOFF_MS / 1000.0) * (2 ** (attempt - 1))
|
|
165
|
+
backoff = backoff * (0.75 + random.random() * 0.5)
|
|
166
|
+
time.sleep(min(backoff, 5.0))
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"name": test["name"],
|
|
171
|
+
"mutation": mutation,
|
|
172
|
+
"request": {
|
|
173
|
+
"method": method,
|
|
174
|
+
"url": request["url"],
|
|
175
|
+
"headers": headers,
|
|
176
|
+
"params": params,
|
|
177
|
+
"body": body if method != "GET" else None,
|
|
178
|
+
},
|
|
179
|
+
"response": {
|
|
180
|
+
"status_code": None,
|
|
181
|
+
"elapsed_ms": elapsed_ms,
|
|
182
|
+
"attempt": attempt,
|
|
183
|
+
"error": last_error,
|
|
184
|
+
"body_snippet": None,
|
|
185
|
+
},
|
|
186
|
+
}
|
{ai_testing_swarm-0.1.1 → ai_testing_swarm-0.1.11}/src/ai_testing_swarm/agents/learning_agent.py
RENAMED
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
4
|
DB = "memory/failure_memory.json"
|
|
5
|
+
MAX_ENTRIES = 200
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class LearningAgent:
|
|
@@ -20,10 +21,11 @@ class LearningAgent:
|
|
|
20
21
|
# Corrupted file → reset
|
|
21
22
|
data = []
|
|
22
23
|
|
|
23
|
-
data.append({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
data.append({"test": test_name, "reason": reasoning})
|
|
25
|
+
|
|
26
|
+
# Keep file bounded
|
|
27
|
+
if len(data) > MAX_ENTRIES:
|
|
28
|
+
data = data[-MAX_ENTRIES:]
|
|
27
29
|
|
|
28
30
|
with open(DB, "w") as f:
|
|
29
31
|
json.dump(data, f, indent=2)
|