ai-testing-swarm 0.1.1__py3-none-any.whl → 0.1.11__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.
@@ -1 +1 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.1.11"
@@ -1,64 +1,186 @@
1
- import requests
1
+ import random
2
+ import time
2
3
  from copy import deepcopy
3
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
+
4
13
 
5
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
+
6
31
  def apply_mutation(self, data: dict, mutation: dict):
7
32
  if not mutation:
8
33
  return deepcopy(data)
9
34
 
10
35
  data = deepcopy(data)
11
- key = mutation["path"][0]
12
- op = mutation["operation"]
36
+ path = mutation.get("path") or []
37
+ op = mutation.get("operation")
13
38
 
14
39
  if op == "REMOVE":
15
- data.pop(key, None)
40
+ self._set_path(data, path, None, remove=True)
16
41
  elif op == "REPLACE":
17
- data[key] = mutation["new_value"]
42
+ self._set_path(data, path, mutation.get("new_value"), remove=False)
18
43
 
19
44
  return data
20
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
+
21
58
  def execute(self, request: dict, test: dict):
22
59
  mutation = test.get("mutation")
23
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
+
24
66
  params = (
25
- self.apply_mutation(request.get("params", {}), mutation)
67
+ self.apply_mutation(request.get("params", {}) or {}, mutation)
26
68
  if mutation and mutation.get("target") == "query"
27
- else deepcopy(request.get("params", {}))
69
+ else deepcopy(request.get("params", {}) or {})
28
70
  )
29
71
 
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
- )
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")
35
77
 
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
- )
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
44
168
 
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
- }
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
+ }
@@ -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
- "test": test_name,
25
- "reason": reasoning
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)