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.
@@ -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"])}
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ai-test = ai_testing_swarm.cli:main
@@ -0,0 +1 @@
1
+ ai_testing_swarm