ai-testing-swarm 0.1.1__py3-none-any.whl
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/__init__.py +1 -0
- ai_testing_swarm/agents/__init__.py +0 -0
- ai_testing_swarm/agents/execution_agent.py +64 -0
- ai_testing_swarm/agents/learning_agent.py +29 -0
- ai_testing_swarm/agents/llm_reasoning_agent.py +162 -0
- ai_testing_swarm/agents/release_gate_agent.py +177 -0
- ai_testing_swarm/agents/test_planner_agent.py +111 -0
- ai_testing_swarm/agents/test_writer_agent.py +22 -0
- ai_testing_swarm/agents/ui_agent.py +24 -0
- ai_testing_swarm/cli.py +78 -0
- ai_testing_swarm/core/__init__.py +0 -0
- ai_testing_swarm/core/api_client.py +19 -0
- ai_testing_swarm/core/curl_parser.py +65 -0
- ai_testing_swarm/orchestrator.py +93 -0
- ai_testing_swarm/reporting/__init__.py +0 -0
- ai_testing_swarm/reporting/report_writer.py +89 -0
- ai_testing_swarm-0.1.1.dist-info/METADATA +55 -0
- ai_testing_swarm-0.1.1.dist-info/RECORD +21 -0
- ai_testing_swarm-0.1.1.dist-info/WHEEL +5 -0
- ai_testing_swarm-0.1.1.dist-info/entry_points.txt +2 -0
- ai_testing_swarm-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ExecutionAgent:
|
|
6
|
+
def apply_mutation(self, data: dict, mutation: dict):
|
|
7
|
+
if not mutation:
|
|
8
|
+
return deepcopy(data)
|
|
9
|
+
|
|
10
|
+
data = deepcopy(data)
|
|
11
|
+
key = mutation["path"][0]
|
|
12
|
+
op = mutation["operation"]
|
|
13
|
+
|
|
14
|
+
if op == "REMOVE":
|
|
15
|
+
data.pop(key, None)
|
|
16
|
+
elif op == "REPLACE":
|
|
17
|
+
data[key] = mutation["new_value"]
|
|
18
|
+
|
|
19
|
+
return data
|
|
20
|
+
|
|
21
|
+
def execute(self, request: dict, test: dict):
|
|
22
|
+
mutation = test.get("mutation")
|
|
23
|
+
|
|
24
|
+
params = (
|
|
25
|
+
self.apply_mutation(request.get("params", {}), mutation)
|
|
26
|
+
if mutation and mutation.get("target") == "query"
|
|
27
|
+
else deepcopy(request.get("params", {}))
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
body = (
|
|
31
|
+
self.apply_mutation(request.get("body", {}), mutation)
|
|
32
|
+
if mutation and mutation.get("target") == "body"
|
|
33
|
+
else deepcopy(request.get("body", {}))
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
response = requests.request(
|
|
37
|
+
method=request["method"],
|
|
38
|
+
url=request["url"],
|
|
39
|
+
headers=request["headers"],
|
|
40
|
+
params=params or None,
|
|
41
|
+
json=None if request["method"] == "GET" else body,
|
|
42
|
+
timeout=10,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
body_snippet = response.json()
|
|
47
|
+
except Exception:
|
|
48
|
+
body_snippet = response.text
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"name": test["name"], # ๐ฅ CANONICAL
|
|
52
|
+
"mutation": mutation,
|
|
53
|
+
"request": {
|
|
54
|
+
"method": request["method"],
|
|
55
|
+
"url": request["url"],
|
|
56
|
+
"headers": request["headers"],
|
|
57
|
+
"params": params,
|
|
58
|
+
"body": body if request["method"] != "GET" else None,
|
|
59
|
+
},
|
|
60
|
+
"response": {
|
|
61
|
+
"status_code": response.status_code,
|
|
62
|
+
"body_snippet": body_snippet,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
DB = "memory/failure_memory.json"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LearningAgent:
|
|
8
|
+
def learn(self, test_name, reasoning):
|
|
9
|
+
os.makedirs(os.path.dirname(DB), exist_ok=True)
|
|
10
|
+
|
|
11
|
+
data = []
|
|
12
|
+
|
|
13
|
+
if os.path.exists(DB):
|
|
14
|
+
try:
|
|
15
|
+
with open(DB, "r") as f:
|
|
16
|
+
content = f.read().strip()
|
|
17
|
+
if content:
|
|
18
|
+
data = json.loads(content)
|
|
19
|
+
except Exception:
|
|
20
|
+
# Corrupted file โ reset
|
|
21
|
+
data = []
|
|
22
|
+
|
|
23
|
+
data.append({
|
|
24
|
+
"test": test_name,
|
|
25
|
+
"reason": reasoning
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
with open(DB, "w") as f:
|
|
29
|
+
json.dump(data, f, indent=2)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLMReasoningAgent
|
|
3
|
+
|
|
4
|
+
โ ๏ธ IMPORTANT DESIGN SHIFT (INTENTIONAL):
|
|
5
|
+
|
|
6
|
+
- Correctness is NEVER decided by an LLM
|
|
7
|
+
- Deterministic HTTP + mutation rules come FIRST
|
|
8
|
+
- This file no longer REQUIRES OpenAI to function
|
|
9
|
+
- LLM can be added later ONLY for explanation enrichment
|
|
10
|
+
|
|
11
|
+
This fixes:
|
|
12
|
+
- happy_path incorrectly marked FAILED
|
|
13
|
+
- random "unknown" classifications
|
|
14
|
+
- non-deterministic release decisions
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LLMReasoningAgent:
|
|
19
|
+
"""
|
|
20
|
+
Classifies API test execution outcomes.
|
|
21
|
+
|
|
22
|
+
INPUT (execution_result):
|
|
23
|
+
{
|
|
24
|
+
"name": str,
|
|
25
|
+
"mutation": dict | None,
|
|
26
|
+
"response": {
|
|
27
|
+
"status_code": int,
|
|
28
|
+
"body": dict | str
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
OUTPUT:
|
|
33
|
+
{
|
|
34
|
+
"type": str,
|
|
35
|
+
"confidence": float,
|
|
36
|
+
"explanation": str
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def reason(self, execution_result: dict) -> dict:
|
|
41
|
+
test_name = execution_result.get("name")
|
|
42
|
+
mutation = execution_result.get("mutation")
|
|
43
|
+
response = execution_result.get("response") or {}
|
|
44
|
+
|
|
45
|
+
status_code = response.get("status_code")
|
|
46
|
+
response_body = response.get("body")
|
|
47
|
+
|
|
48
|
+
# =========================================================
|
|
49
|
+
# 1๏ธโฃ HARD SUCCESS RULE โ HAPPY PATH
|
|
50
|
+
# =========================================================
|
|
51
|
+
if (
|
|
52
|
+
test_name == "happy_path"
|
|
53
|
+
and mutation is None
|
|
54
|
+
and isinstance(status_code, int)
|
|
55
|
+
and 200 <= status_code < 300
|
|
56
|
+
):
|
|
57
|
+
return {
|
|
58
|
+
"type": "success",
|
|
59
|
+
"confidence": 1.0,
|
|
60
|
+
"explanation": "Happy path executed successfully (2xx response)"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# =========================================================
|
|
64
|
+
# 2๏ธโฃ NO RESPONSE / TRANSPORT FAILURE
|
|
65
|
+
# =========================================================
|
|
66
|
+
if status_code is None:
|
|
67
|
+
return {
|
|
68
|
+
"type": "unknown",
|
|
69
|
+
"confidence": 0.3,
|
|
70
|
+
"explanation": "No HTTP response received from server"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# =========================================================
|
|
74
|
+
# 3๏ธโฃ HTTP-LEVEL HARD RULES (NO AI)
|
|
75
|
+
# =========================================================
|
|
76
|
+
if status_code == 405:
|
|
77
|
+
return {
|
|
78
|
+
"type": "method_not_allowed",
|
|
79
|
+
"confidence": 1.0,
|
|
80
|
+
"explanation": "HTTP 405 Method Not Allowed"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if status_code in (401, 403):
|
|
84
|
+
return {
|
|
85
|
+
"type": "auth_issue",
|
|
86
|
+
"confidence": 1.0,
|
|
87
|
+
"explanation": "Authentication or authorization failure"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if status_code >= 500:
|
|
91
|
+
return {
|
|
92
|
+
"type": "infra",
|
|
93
|
+
"confidence": 1.0,
|
|
94
|
+
"explanation": "Server-side 5xx failure"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# =====================================================
|
|
98
|
+
# 4๏ธโฃ MUTATION-AWARE SEMANTIC CLASSIFICATION
|
|
99
|
+
# =====================================================
|
|
100
|
+
if mutation:
|
|
101
|
+
strategy = mutation.get("strategy")
|
|
102
|
+
|
|
103
|
+
if status_code == 400 and strategy == "missing_param":
|
|
104
|
+
return {
|
|
105
|
+
"type": "missing_param",
|
|
106
|
+
"confidence": 0.9,
|
|
107
|
+
"explanation": "400 response after required parameter removal"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if status_code == 400 and strategy == "null_param":
|
|
111
|
+
return {
|
|
112
|
+
"type": "missing_param",
|
|
113
|
+
"confidence": 0.9,
|
|
114
|
+
"explanation": "400 response after nullifying parameter"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if status_code == 400 and strategy == "invalid_param":
|
|
118
|
+
return {
|
|
119
|
+
"type": "invalid_param",
|
|
120
|
+
"confidence": 0.9,
|
|
121
|
+
"explanation": "400 response after invalid parameter mutation"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# โ
FIX: security payload blocked โ SAFE
|
|
125
|
+
if status_code >= 400 and status_code < 500 and strategy == "security":
|
|
126
|
+
return {
|
|
127
|
+
"type": "security",
|
|
128
|
+
"confidence": 1.0,
|
|
129
|
+
"explanation": (
|
|
130
|
+
"Security payload was rejected with client error "
|
|
131
|
+
"(treated as safe input validation)"
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# ๐จ security payload accepted โ RISK
|
|
136
|
+
if status_code >= 200 and status_code < 300 and strategy == "security":
|
|
137
|
+
return {
|
|
138
|
+
"type": "security_risk",
|
|
139
|
+
"confidence": 1.0,
|
|
140
|
+
"explanation": (
|
|
141
|
+
"Security payload was accepted (potential vulnerability)"
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# =========================================================
|
|
146
|
+
# 5๏ธโฃ BUSINESS LOGIC / GENERIC SUCCESS
|
|
147
|
+
# =========================================================
|
|
148
|
+
if 200 <= status_code < 300:
|
|
149
|
+
return {
|
|
150
|
+
"type": "success",
|
|
151
|
+
"confidence": 0.8,
|
|
152
|
+
"explanation": "2xx response without mutation-induced failure"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# =========================================================
|
|
156
|
+
# 6๏ธโฃ FALLBACK
|
|
157
|
+
# =========================================================
|
|
158
|
+
return {
|
|
159
|
+
"type": "unknown",
|
|
160
|
+
"confidence": 0.6,
|
|
161
|
+
"explanation": "No deterministic rule matched this response"
|
|
162
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# class ReleaseGateAgent:
|
|
2
|
+
# """
|
|
3
|
+
# Release decision engine based on mutation testing semantics.
|
|
4
|
+
#
|
|
5
|
+
# IMPORTANT:
|
|
6
|
+
# - Test FAIL does NOT mean system FAIL
|
|
7
|
+
# - Only unexpected behavior blocks release
|
|
8
|
+
# """
|
|
9
|
+
#
|
|
10
|
+
# # โ These mean SYSTEM IS BROKEN
|
|
11
|
+
# BLOCKING_FAILURES = {
|
|
12
|
+
# "auth_issue", # auth broken
|
|
13
|
+
# "infra", # infra / network / 5xx
|
|
14
|
+
# "security_risk", # malicious payload succeeded (2xx/3xx)
|
|
15
|
+
# "server_error",
|
|
16
|
+
# }
|
|
17
|
+
#
|
|
18
|
+
# # โ ๏ธ Ambiguous behavior (release with caution)
|
|
19
|
+
# RISKY_FAILURES = {
|
|
20
|
+
# "unknown",
|
|
21
|
+
# }
|
|
22
|
+
#
|
|
23
|
+
# # โ
EXPECTED & HEALTHY system behavior
|
|
24
|
+
# EXPECTED_FAILURES = {
|
|
25
|
+
# "success",
|
|
26
|
+
# "missing_param",
|
|
27
|
+
# "invalid_param",
|
|
28
|
+
# "security",
|
|
29
|
+
# }
|
|
30
|
+
#
|
|
31
|
+
# def decide(self, results: list) -> str:
|
|
32
|
+
# happy_path_ok = False
|
|
33
|
+
# has_blocker = False
|
|
34
|
+
# has_risk = False
|
|
35
|
+
#
|
|
36
|
+
# for r in results:
|
|
37
|
+
# test_name = r.get("test_name")
|
|
38
|
+
# failure_type = r.get("failure_type")
|
|
39
|
+
# status_code = r.get("response", {}).get("status_code")
|
|
40
|
+
#
|
|
41
|
+
# # -------------------------------
|
|
42
|
+
# # Happy path MUST succeed
|
|
43
|
+
# # -------------------------------
|
|
44
|
+
# if test_name == "happy_path":
|
|
45
|
+
# if failure_type == "success" and status_code and status_code < 400:
|
|
46
|
+
# happy_path_ok = True
|
|
47
|
+
# else:
|
|
48
|
+
# return "REJECT_RELEASE"
|
|
49
|
+
#
|
|
50
|
+
# # -------------------------------
|
|
51
|
+
# # Blocking issues
|
|
52
|
+
# # -------------------------------
|
|
53
|
+
# if failure_type in self.BLOCKING_FAILURES:
|
|
54
|
+
# has_blocker = True
|
|
55
|
+
#
|
|
56
|
+
# # -------------------------------
|
|
57
|
+
# # Risky but not blocking
|
|
58
|
+
# # -------------------------------
|
|
59
|
+
# elif failure_type in self.RISKY_FAILURES:
|
|
60
|
+
# has_risk = True
|
|
61
|
+
#
|
|
62
|
+
# # EXPECTED_FAILURES are intentionally ignored
|
|
63
|
+
#
|
|
64
|
+
# # -------------------------------
|
|
65
|
+
# # Final decision
|
|
66
|
+
# # -------------------------------
|
|
67
|
+
# if has_blocker:
|
|
68
|
+
# return "REJECT_RELEASE"
|
|
69
|
+
#
|
|
70
|
+
# if has_risk:
|
|
71
|
+
# return "APPROVE_RELEASE_WITH_RISKS"
|
|
72
|
+
#
|
|
73
|
+
# return "APPROVE_RELEASE"
|
|
74
|
+
import logging
|
|
75
|
+
|
|
76
|
+
logger = logging.getLogger(__name__)
|
|
77
|
+
|
|
78
|
+
class ReleaseGateAgent:
|
|
79
|
+
"""
|
|
80
|
+
Release decision engine based on mutation testing semantics.
|
|
81
|
+
|
|
82
|
+
IMPORTANT:
|
|
83
|
+
- Test FAIL does NOT mean system FAIL
|
|
84
|
+
- Only unexpected behavior blocks release
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
# โ These mean SYSTEM IS BROKEN
|
|
88
|
+
BLOCKING_FAILURES = {
|
|
89
|
+
"auth_issue", # auth broken
|
|
90
|
+
"infra", # infra / network / 5xx
|
|
91
|
+
"security_risk", # malicious payload succeeded (2xx/3xx)
|
|
92
|
+
"server_error",
|
|
93
|
+
"method_not_allowed",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# โ ๏ธ Ambiguous behavior (release with caution)
|
|
97
|
+
RISKY_FAILURES = {
|
|
98
|
+
"unknown",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# โ
EXPECTED & HEALTHY system behavior
|
|
102
|
+
EXPECTED_FAILURES = {
|
|
103
|
+
"success",
|
|
104
|
+
"missing_param",
|
|
105
|
+
"invalid_param",
|
|
106
|
+
"security", # ๐ฅ IMPORTANT: security blocked = SAFE
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def decide(self, results: list) -> str:
|
|
110
|
+
happy_path_ok = False
|
|
111
|
+
has_blocker = False
|
|
112
|
+
has_risk = False
|
|
113
|
+
|
|
114
|
+
for r in results:
|
|
115
|
+
# โ
FIX 1: correct key
|
|
116
|
+
test_name = r.get("name") or r.get("test_name")
|
|
117
|
+
failure_type = r.get("failure_type")
|
|
118
|
+
status_code = r.get("response", {}).get("status_code")
|
|
119
|
+
|
|
120
|
+
logger.info(
|
|
121
|
+
"๐ Evaluating test=%s | failure_type=%s | status_code=%s",
|
|
122
|
+
test_name,
|
|
123
|
+
failure_type,
|
|
124
|
+
status_code,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# -------------------------------
|
|
128
|
+
# Happy path MUST succeed
|
|
129
|
+
# -------------------------------
|
|
130
|
+
if test_name == "happy_path":
|
|
131
|
+
if failure_type == "success" and status_code and status_code < 400:
|
|
132
|
+
happy_path_ok = True
|
|
133
|
+
logger.info("โ
Happy path passed")
|
|
134
|
+
else:
|
|
135
|
+
logger.error(
|
|
136
|
+
"โ Happy path failed โ failure_type=%s status_code=%s",
|
|
137
|
+
failure_type,
|
|
138
|
+
status_code,
|
|
139
|
+
)
|
|
140
|
+
return "REJECT_RELEASE"
|
|
141
|
+
|
|
142
|
+
# -------------------------------
|
|
143
|
+
# Blocking issues
|
|
144
|
+
# -------------------------------
|
|
145
|
+
if failure_type in self.BLOCKING_FAILURES:
|
|
146
|
+
logger.error(
|
|
147
|
+
"๐ซ BLOCKING FAILURE DETECTED โ test=%s | failure_type=%s | status_code=%s",
|
|
148
|
+
test_name,
|
|
149
|
+
failure_type,
|
|
150
|
+
status_code,
|
|
151
|
+
)
|
|
152
|
+
has_blocker = True
|
|
153
|
+
|
|
154
|
+
# -------------------------------
|
|
155
|
+
# Risky but not blocking
|
|
156
|
+
# -------------------------------
|
|
157
|
+
elif failure_type in self.RISKY_FAILURES:
|
|
158
|
+
logger.warning(
|
|
159
|
+
"โ ๏ธ Risky behavior detected โ test=%s | failure_type=%s",
|
|
160
|
+
test_name,
|
|
161
|
+
failure_type,
|
|
162
|
+
)
|
|
163
|
+
has_risk = True
|
|
164
|
+
|
|
165
|
+
# -------------------------------
|
|
166
|
+
# Final decision
|
|
167
|
+
# -------------------------------
|
|
168
|
+
if not happy_path_ok:
|
|
169
|
+
return "REJECT_RELEASE"
|
|
170
|
+
|
|
171
|
+
if has_blocker:
|
|
172
|
+
return "REJECT_RELEASE"
|
|
173
|
+
|
|
174
|
+
if has_risk:
|
|
175
|
+
return "APPROVE_RELEASE_WITH_RISKS"
|
|
176
|
+
|
|
177
|
+
return "APPROVE_RELEASE"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
class TestPlannerAgent:
|
|
2
|
+
"""
|
|
3
|
+
Generic test planner.
|
|
4
|
+
- Always includes happy path
|
|
5
|
+
- GET โ query param mutations
|
|
6
|
+
- POST โ body mutations
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def plan(self, request: dict):
|
|
10
|
+
method = request["method"].upper()
|
|
11
|
+
params = request.get("params", {}) or {}
|
|
12
|
+
body = request.get("body", {}) or {}
|
|
13
|
+
|
|
14
|
+
tests = []
|
|
15
|
+
|
|
16
|
+
# โ
POSITIVE CASE (ALWAYS)
|
|
17
|
+
tests.append({
|
|
18
|
+
"name": "happy_path",
|
|
19
|
+
"mutation": None,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
# ---------------- GET QUERY PARAM MUTATIONS ----------------
|
|
23
|
+
if method == "GET" and params:
|
|
24
|
+
for key, value in params.items():
|
|
25
|
+
tests.extend([
|
|
26
|
+
{
|
|
27
|
+
"name": f"missing_param_{key}",
|
|
28
|
+
"mutation": {
|
|
29
|
+
"target": "query",
|
|
30
|
+
"path": [key],
|
|
31
|
+
"operation": "REMOVE",
|
|
32
|
+
"original_value": value,
|
|
33
|
+
"new_value": None,
|
|
34
|
+
"strategy": "missing_param",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": f"empty_param_{key}",
|
|
39
|
+
"mutation": {
|
|
40
|
+
"target": "query",
|
|
41
|
+
"path": [key],
|
|
42
|
+
"operation": "REPLACE",
|
|
43
|
+
"original_value": value,
|
|
44
|
+
"new_value": "",
|
|
45
|
+
"strategy": "invalid_param",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": f"invalid_param_{key}",
|
|
50
|
+
"mutation": {
|
|
51
|
+
"target": "query",
|
|
52
|
+
"path": [key],
|
|
53
|
+
"operation": "REPLACE",
|
|
54
|
+
"original_value": value,
|
|
55
|
+
"new_value": "INVALID",
|
|
56
|
+
"strategy": "invalid_param",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": f"security_{key}",
|
|
61
|
+
"mutation": {
|
|
62
|
+
"target": "query",
|
|
63
|
+
"path": [key],
|
|
64
|
+
"operation": "REPLACE",
|
|
65
|
+
"original_value": value,
|
|
66
|
+
"new_value": "' OR 1=1 --",
|
|
67
|
+
"strategy": "security",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
# ---------------- BODY MUTATIONS ----------------
|
|
73
|
+
if method != "GET" and body:
|
|
74
|
+
for key, value in body.items():
|
|
75
|
+
tests.extend([
|
|
76
|
+
{
|
|
77
|
+
"name": f"missing_param_{key}",
|
|
78
|
+
"mutation": {
|
|
79
|
+
"target": "body",
|
|
80
|
+
"path": [key],
|
|
81
|
+
"operation": "REMOVE",
|
|
82
|
+
"original_value": value,
|
|
83
|
+
"new_value": None,
|
|
84
|
+
"strategy": "missing_param",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": f"invalid_param_{key}",
|
|
89
|
+
"mutation": {
|
|
90
|
+
"target": "body",
|
|
91
|
+
"path": [key],
|
|
92
|
+
"operation": "REPLACE",
|
|
93
|
+
"original_value": value,
|
|
94
|
+
"new_value": "INVALID",
|
|
95
|
+
"strategy": "invalid_param",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": f"security_{key}",
|
|
100
|
+
"mutation": {
|
|
101
|
+
"target": "body",
|
|
102
|
+
"path": [key],
|
|
103
|
+
"operation": "REPLACE",
|
|
104
|
+
"original_value": value,
|
|
105
|
+
"new_value": "' OR 1=1 --",
|
|
106
|
+
"strategy": "security",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
return tests
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestWriterAgent:
|
|
5
|
+
def write(self, request, plan):
|
|
6
|
+
tests = []
|
|
7
|
+
|
|
8
|
+
for step in plan:
|
|
9
|
+
if step == "happy_path":
|
|
10
|
+
tests.append(("happy_path", request))
|
|
11
|
+
else:
|
|
12
|
+
for key in request["body"].keys():
|
|
13
|
+
r = copy.deepcopy(request)
|
|
14
|
+
if step == "missing_param":
|
|
15
|
+
r["body"].pop(key, None)
|
|
16
|
+
elif step == "null_param":
|
|
17
|
+
r["body"][key] = None
|
|
18
|
+
elif step == "wrong_type":
|
|
19
|
+
r["body"][key] = "INVALID"
|
|
20
|
+
tests.append((f"{step}_{key}", r))
|
|
21
|
+
|
|
22
|
+
return tests
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class UIAgent:
|
|
2
|
+
def validate(self, api_response):
|
|
3
|
+
try:
|
|
4
|
+
from playwright.sync_api import sync_playwright
|
|
5
|
+
except ImportError:
|
|
6
|
+
return {"ui": "skipped"}
|
|
7
|
+
|
|
8
|
+
if "order_id" not in api_response:
|
|
9
|
+
return {"ui": "not_applicable"}
|
|
10
|
+
|
|
11
|
+
with sync_playwright() as p:
|
|
12
|
+
browser = p.chromium.launch(headless=True)
|
|
13
|
+
page = browser.new_page()
|
|
14
|
+
|
|
15
|
+
page.goto("https://your-ui.com/orders")
|
|
16
|
+
page.fill("#order-search", str(api_response["order_id"]))
|
|
17
|
+
page.click("#search")
|
|
18
|
+
|
|
19
|
+
page.wait_for_selector(".order-id", timeout=5000)
|
|
20
|
+
visible_id = page.inner_text(".order-id")
|
|
21
|
+
|
|
22
|
+
browser.close()
|
|
23
|
+
|
|
24
|
+
return {"ui": visible_id == str(api_response["order_id"])}
|
ai_testing_swarm/cli.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
from ai_testing_swarm.orchestrator import SwarmOrchestrator
|
|
4
|
+
from ai_testing_swarm.core.curl_parser import parse_curl
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalize_request(payload: dict) -> dict:
|
|
8
|
+
"""
|
|
9
|
+
Normalize input into execution-ready request.
|
|
10
|
+
|
|
11
|
+
Supported formats:
|
|
12
|
+
1. { "curl": "curl ..." }
|
|
13
|
+
2. { "method": "...", "url": "...", "headers": {...}, "body": {...} }
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Case 1: raw curl input
|
|
17
|
+
if "curl" in payload:
|
|
18
|
+
return parse_curl(payload["curl"])
|
|
19
|
+
|
|
20
|
+
# Case 2: already normalized
|
|
21
|
+
required_keys = {"method", "url"}
|
|
22
|
+
if required_keys.issubset(payload.keys()):
|
|
23
|
+
return payload
|
|
24
|
+
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"Invalid input format.\n"
|
|
27
|
+
"Expected either:\n"
|
|
28
|
+
"1) { \"curl\": \"curl ...\" }\n"
|
|
29
|
+
"2) { \"method\": \"POST\", \"url\": \"...\", \"headers\": {}, \"body\": {} }"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
parser = argparse.ArgumentParser(description="AI Testing Swarm CLI")
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--input",
|
|
37
|
+
required=True,
|
|
38
|
+
help="Path to request.json"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------
|
|
44
|
+
# Load input JSON
|
|
45
|
+
# ------------------------------------------------------------
|
|
46
|
+
with open(args.input) as f:
|
|
47
|
+
payload = json.load(f)
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------
|
|
50
|
+
# ๐ด NORMALIZE INPUT (THIS FIXES YOUR ERROR)
|
|
51
|
+
# ------------------------------------------------------------
|
|
52
|
+
request = normalize_request(payload)
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------
|
|
55
|
+
# Run swarm
|
|
56
|
+
# ------------------------------------------------------------
|
|
57
|
+
decision, results = SwarmOrchestrator().run(request)
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------
|
|
60
|
+
# Console output
|
|
61
|
+
# ------------------------------------------------------------
|
|
62
|
+
print("\n=== RELEASE DECISION ===")
|
|
63
|
+
print(decision)
|
|
64
|
+
|
|
65
|
+
print("\n=== TEST RESULTS ===")
|
|
66
|
+
for r in results:
|
|
67
|
+
response = r.get("response", {})
|
|
68
|
+
status_code = response.get("status_code")
|
|
69
|
+
|
|
70
|
+
print(
|
|
71
|
+
f"{r.get('name'):25} "
|
|
72
|
+
f"{str(status_code):5} "
|
|
73
|
+
f"{r.get('reason')}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def send_request(method, url, headers=None, body=None, allow_redirects=False):
|
|
5
|
+
response = requests.request(
|
|
6
|
+
method=method,
|
|
7
|
+
url=url,
|
|
8
|
+
headers=headers,
|
|
9
|
+
json=body,
|
|
10
|
+
timeout=10,
|
|
11
|
+
allow_redirects=allow_redirects
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
response_json = response.json()
|
|
16
|
+
except Exception:
|
|
17
|
+
response_json = {}
|
|
18
|
+
|
|
19
|
+
return response, response_json
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import urllib.parse
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_curl(curl_command: str) -> dict:
|
|
7
|
+
tokens = shlex.split(curl_command)
|
|
8
|
+
|
|
9
|
+
method = "GET"
|
|
10
|
+
url = ""
|
|
11
|
+
headers = {}
|
|
12
|
+
body = None
|
|
13
|
+
|
|
14
|
+
i = 0
|
|
15
|
+
while i < len(tokens):
|
|
16
|
+
token = tokens[i]
|
|
17
|
+
|
|
18
|
+
if token in ("-X", "--request"):
|
|
19
|
+
method = tokens[i + 1].upper()
|
|
20
|
+
i += 2
|
|
21
|
+
continue
|
|
22
|
+
|
|
23
|
+
if token.startswith("http"):
|
|
24
|
+
url = token
|
|
25
|
+
i += 1
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
if token in ("-H", "--header"):
|
|
29
|
+
header = tokens[i + 1]
|
|
30
|
+
key, value = header.split(":", 1)
|
|
31
|
+
headers[key.strip().lower()] = value.strip()
|
|
32
|
+
i += 2
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
if token in ("-d", "--data", "--data-raw"):
|
|
36
|
+
body = tokens[i + 1]
|
|
37
|
+
method = "POST"
|
|
38
|
+
i += 2
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
i += 1
|
|
42
|
+
|
|
43
|
+
# -------------------- URL PARSING (CRITICAL) --------------------
|
|
44
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
45
|
+
|
|
46
|
+
params = dict(urllib.parse.parse_qsl(parsed_url.query))
|
|
47
|
+
|
|
48
|
+
clean_url = urllib.parse.urlunparse(
|
|
49
|
+
parsed_url._replace(query="")
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
parsed_body = {}
|
|
53
|
+
if body:
|
|
54
|
+
try:
|
|
55
|
+
parsed_body = json.loads(body)
|
|
56
|
+
except Exception:
|
|
57
|
+
parsed_body = body
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"method": method,
|
|
61
|
+
"url": clean_url,
|
|
62
|
+
"headers": headers,
|
|
63
|
+
"params": params, # ๐ฅ THIS UNBLOCKS ALL TESTS
|
|
64
|
+
"body": parsed_body or {},
|
|
65
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from ai_testing_swarm.agents.test_planner_agent import TestPlannerAgent
|
|
2
|
+
from ai_testing_swarm.agents.execution_agent import ExecutionAgent
|
|
3
|
+
from ai_testing_swarm.agents.llm_reasoning_agent import LLMReasoningAgent
|
|
4
|
+
from ai_testing_swarm.agents.learning_agent import LearningAgent
|
|
5
|
+
from ai_testing_swarm.agents.release_gate_agent import ReleaseGateAgent
|
|
6
|
+
from ai_testing_swarm.reporting.report_writer import write_report
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
EXPECTED_FAILURES = {
|
|
12
|
+
"success",
|
|
13
|
+
"missing_param",
|
|
14
|
+
"invalid_param",
|
|
15
|
+
"security",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class SwarmOrchestrator:
|
|
19
|
+
"""
|
|
20
|
+
Central brain of the AI Testing Swarm.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.planner = TestPlannerAgent()
|
|
25
|
+
self.executor = ExecutionAgent()
|
|
26
|
+
self.reasoner = LLMReasoningAgent()
|
|
27
|
+
self.learner = LearningAgent()
|
|
28
|
+
self.release_gate = ReleaseGateAgent()
|
|
29
|
+
|
|
30
|
+
def run(self, request: dict):
|
|
31
|
+
"""
|
|
32
|
+
Runs the full AI testing swarm and returns:
|
|
33
|
+
- release decision
|
|
34
|
+
- full test results
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# --------------------------------------------------------
|
|
38
|
+
# 1๏ธโฃ PLAN TESTS
|
|
39
|
+
# --------------------------------------------------------
|
|
40
|
+
tests = self.planner.plan(request)
|
|
41
|
+
results = []
|
|
42
|
+
|
|
43
|
+
logger.info("๐ง Planned %d tests", len(tests))
|
|
44
|
+
|
|
45
|
+
# --------------------------------------------------------
|
|
46
|
+
# 2๏ธโฃ EXECUTE + CLASSIFY
|
|
47
|
+
# --------------------------------------------------------
|
|
48
|
+
for test in tests:
|
|
49
|
+
test_name = test.get("name")
|
|
50
|
+
|
|
51
|
+
# Execute request with mutation (or baseline)
|
|
52
|
+
execution_result = self.executor.execute(request, test)
|
|
53
|
+
|
|
54
|
+
# ๐ GUARANTEE name is always present
|
|
55
|
+
execution_result["name"] = test_name
|
|
56
|
+
|
|
57
|
+
# Classify result (LLM / rules)
|
|
58
|
+
classification = self.reasoner.reason(execution_result)
|
|
59
|
+
|
|
60
|
+
# Merge classification into execution result
|
|
61
|
+
execution_result.update({
|
|
62
|
+
"reason": classification.get("type"),
|
|
63
|
+
"confidence": classification.get("confidence", 1.0),
|
|
64
|
+
"failure_type": classification.get("type"),
|
|
65
|
+
"status": (
|
|
66
|
+
"PASSED"
|
|
67
|
+
# if classification.get("type") == "success"
|
|
68
|
+
if classification["type"] in EXPECTED_FAILURES
|
|
69
|
+
else "FAILED"
|
|
70
|
+
),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
results.append(execution_result)
|
|
74
|
+
|
|
75
|
+
# Optional learning step
|
|
76
|
+
try:
|
|
77
|
+
self.learner.learn(test_name, classification)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.warning("โ ๏ธ Learning skipped for %s: %s", test_name, e)
|
|
80
|
+
|
|
81
|
+
# --------------------------------------------------------
|
|
82
|
+
# 3๏ธโฃ WRITE JSON REPORT (ONCE, ONLY ONCE)
|
|
83
|
+
# --------------------------------------------------------
|
|
84
|
+
report_path = write_report(request, results)
|
|
85
|
+
logger.info("๐ Swarm JSON report written to: %s", report_path)
|
|
86
|
+
print(f"๐ Swarm JSON report written to: {report_path}")
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------
|
|
89
|
+
# 4๏ธโฃ RELEASE DECISION
|
|
90
|
+
# --------------------------------------------------------
|
|
91
|
+
decision = self.release_gate.decide(results)
|
|
92
|
+
|
|
93
|
+
return decision, results
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ============================================================
|
|
10
|
+
# ๐ FIND CALLER PROJECT ROOT (NOT PACKAGE ROOT)
|
|
11
|
+
# ============================================================
|
|
12
|
+
def find_execution_root() -> Path:
|
|
13
|
+
"""
|
|
14
|
+
Resolve project root based on WHERE tests are executed from,
|
|
15
|
+
not where this package lives.
|
|
16
|
+
"""
|
|
17
|
+
current = Path.cwd().resolve()
|
|
18
|
+
|
|
19
|
+
while current != current.parent:
|
|
20
|
+
if any(
|
|
21
|
+
(current / marker).exists()
|
|
22
|
+
for marker in (
|
|
23
|
+
"pyproject.toml",
|
|
24
|
+
"setup.py",
|
|
25
|
+
"requirements.txt",
|
|
26
|
+
".git",
|
|
27
|
+
)
|
|
28
|
+
):
|
|
29
|
+
return current
|
|
30
|
+
current = current.parent
|
|
31
|
+
|
|
32
|
+
# Fallback: use cwd directly
|
|
33
|
+
return Path.cwd().resolve()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
PROJECT_ROOT = find_execution_root()
|
|
37
|
+
REPORTS_DIR = PROJECT_ROOT / "ai_swarm_reports"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ============================================================
|
|
41
|
+
# ๐งน UTILS
|
|
42
|
+
# ============================================================
|
|
43
|
+
def extract_endpoint_name(method: str, url: str) -> str:
|
|
44
|
+
"""
|
|
45
|
+
POST https://preprod-api.getepichome.in/api/validate-gst/
|
|
46
|
+
-> POST_validate-gst
|
|
47
|
+
"""
|
|
48
|
+
parsed = urlparse(url)
|
|
49
|
+
parts = [p for p in parsed.path.split("/") if p]
|
|
50
|
+
|
|
51
|
+
endpoint = parts[-1] if parts else "root"
|
|
52
|
+
endpoint = re.sub(r"[^a-zA-Z0-9_-]", "-", endpoint)
|
|
53
|
+
|
|
54
|
+
return f"{method}_{endpoint}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ============================================================
|
|
58
|
+
# ๐ REPORT WRITER
|
|
59
|
+
# ============================================================
|
|
60
|
+
def write_report(request: dict, results: list) -> str:
|
|
61
|
+
REPORTS_DIR.mkdir(exist_ok=True)
|
|
62
|
+
|
|
63
|
+
method = request.get("method", "UNKNOWN")
|
|
64
|
+
url = request.get("url", "")
|
|
65
|
+
|
|
66
|
+
endpoint_name = extract_endpoint_name(method, url)
|
|
67
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
68
|
+
|
|
69
|
+
# report_path = REPORTS_DIR / f"{timestamp}_{endpoint_name}.json"
|
|
70
|
+
# ๐ฅ PER-ENDPOINT FOLDER
|
|
71
|
+
endpoint_dir = REPORTS_DIR / endpoint_name
|
|
72
|
+
endpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
# ๐ฅ FILE NAME FORMAT
|
|
75
|
+
report_path = endpoint_dir / f"{endpoint_name}_{timestamp}.json"
|
|
76
|
+
|
|
77
|
+
report = {
|
|
78
|
+
"endpoint": f"{method} {url}",
|
|
79
|
+
"run_time": timestamp,
|
|
80
|
+
"total_tests": len(results),
|
|
81
|
+
"results": results,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
with open(report_path, "w") as f:
|
|
85
|
+
json.dump(report, f, indent=2)
|
|
86
|
+
|
|
87
|
+
print(f"๐ Swarm JSON report written to: {report_path}")
|
|
88
|
+
|
|
89
|
+
return str(report_path)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-testing-swarm
|
|
3
|
+
Version: 0.1.1
|
|
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
|
+
Autonomous AI-powered API & UI testing platform.
|
|
13
|
+
|
|
14
|
+
## Run locally
|
|
15
|
+
pip install -e .
|
|
16
|
+
export OPENAI_API_KEY=your_key
|
|
17
|
+
ai-test --input input/request.json
|
|
18
|
+
|
|
19
|
+
## Pytest + Allure
|
|
20
|
+
pytest --alluredir=allure-results
|
|
21
|
+
allure serve allure-results
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ai-testing-swarm/
|
|
25
|
+
โ
|
|
26
|
+
โโโ ai_testing_swarm/
|
|
27
|
+
โ โโโ __init__.py
|
|
28
|
+
โ โ
|
|
29
|
+
โ โโโ core/
|
|
30
|
+
โ โ โโโ curl_parser.py
|
|
31
|
+
โ โ โโโ api_client.py
|
|
32
|
+
โ โ
|
|
33
|
+
โ โโโ agents/
|
|
34
|
+
โ โ โโโ test_planner_agent.py
|
|
35
|
+
โ โ โโโ test_writer_agent.py
|
|
36
|
+
โ โ โโโ execution_agent.py
|
|
37
|
+
โ โ โโโ llm_reasoning_agent.py
|
|
38
|
+
โ โ โโโ learning_agent.py
|
|
39
|
+
โ โ โโโ release_gate_agent.py
|
|
40
|
+
โ โ โโโ ui_agent.py ๐ Playwright auto-plug
|
|
41
|
+
โ โ
|
|
42
|
+
โ โโโ orchestrator.py
|
|
43
|
+
โ
|
|
44
|
+
โ โโโ cli.py ๐ ai-test command
|
|
45
|
+
โ
|
|
46
|
+
โโโ tests/
|
|
47
|
+
โ โโโ test_swarm_api.py ๐ pytest + allure
|
|
48
|
+
โ
|
|
49
|
+
โโโ memory/
|
|
50
|
+
โ โโโ failure_memory.json
|
|
51
|
+
โ
|
|
52
|
+
โโโ pyproject.toml ๐ packaging
|
|
53
|
+
โโโ README.md
|
|
54
|
+
โโโ input/
|
|
55
|
+
โโโ request.json
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
ai_testing_swarm/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
ai_testing_swarm/cli.py,sha256=Ug_8F4LCfj0qqXIzqjzwIi2lEzW41VS2SviB5z7Rb0E,2269
|
|
3
|
+
ai_testing_swarm/orchestrator.py,sha256=96WqcLSvFnMUQ8O4Rxt4FaOBgf6pJBwMzgvJAxg_2BA,3381
|
|
4
|
+
ai_testing_swarm/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
ai_testing_swarm/agents/execution_agent.py,sha256=BT0wqPKagF54Pd-ORHei3wb3_CAZSpoas77O40qDrJA,1885
|
|
6
|
+
ai_testing_swarm/agents/learning_agent.py,sha256=Py9uRdWlTKEHwMINyH4lJ0TNO4JP_w6k7F7IRLmb16A,689
|
|
7
|
+
ai_testing_swarm/agents/llm_reasoning_agent.py,sha256=ESuli7edRW73RVewQBriSFCLsp_I476xdZA6hE2QqH4,5538
|
|
8
|
+
ai_testing_swarm/agents/release_gate_agent.py,sha256=SSSuRYWtYs06VYkovyiMp3W3wNraAHY3840xUTU5ums,5605
|
|
9
|
+
ai_testing_swarm/agents/test_planner_agent.py,sha256=3Wr57Gdrw8unyOjLSIowzZtyzhzpsHBuS71E53U56QQ,4167
|
|
10
|
+
ai_testing_swarm/agents/test_writer_agent.py,sha256=tOCeUv01cl3t4vD8N8oXqVpxUFSE42agky5R9Dn2WVE,691
|
|
11
|
+
ai_testing_swarm/agents/ui_agent.py,sha256=sNCTFbDIxkU8woKGoCHlUi83IRJCJrbxvkTYcSrB0EY,781
|
|
12
|
+
ai_testing_swarm/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
ai_testing_swarm/core/api_client.py,sha256=ENAL5W7Rg2PsggjvgEE301a4PXuBKpgU-5O107gGeQQ,415
|
|
14
|
+
ai_testing_swarm/core/curl_parser.py,sha256=0dPEhRGqn-4u00t-bp7kW9Ii7GSiO0wteB4kDhRKUBQ,1483
|
|
15
|
+
ai_testing_swarm/reporting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
ai_testing_swarm/reporting/report_writer.py,sha256=drPRdxIgkXCcZifCAM3Y1FhR2ZE2ap0OnSQQlZQRiU4,2547
|
|
17
|
+
ai_testing_swarm-0.1.1.dist-info/METADATA,sha256=00ctM45ApkGBaJbCxVZzCBAaXenWHYgwkgH14l5-lsI,1399
|
|
18
|
+
ai_testing_swarm-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
19
|
+
ai_testing_swarm-0.1.1.dist-info/entry_points.txt,sha256=vbW-IBcVcls5I-NA3xFUZxH4Ktevt7lA4w9P4Me0yXo,54
|
|
20
|
+
ai_testing_swarm-0.1.1.dist-info/top_level.txt,sha256=OSqbej3vG04SKqgEcgzDTMn8QzpVsxwOzpSG7quhWJw,17
|
|
21
|
+
ai_testing_swarm-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ai_testing_swarm
|