synapse-hub 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.agents/agents/orchestrator.md +7 -0
  2. package/.agents/rules/lessons-learned.md +1 -0
  3. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +1 -1
  4. package/.agents/skills/antigravity-test-sentinel/SKILL.md +1 -2
  5. package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
  6. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +11 -4
  7. package/.agents/skills/architecture-decision-records/SKILL.md +1 -1
  8. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
  9. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +32 -11
  10. package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
  11. package/.agents/skills/clean-pom-crafter/SKILL.md +1 -4
  12. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +1 -1
  13. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +1 -1
  14. package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
  15. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +1 -3
  16. package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
  17. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +1 -1
  18. package/.agents/skills/github-actions-templates/SKILL.md +1 -1
  19. package/.agents/skills/graphify-impact-analyzer/SKILL.md +1 -3
  20. package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
  21. package/.agents/skills/lam-script-generator/SKILL.md +1 -2
  22. package/.agents/skills/playwright-live-interaction/SKILL.md +1 -4
  23. package/.agents/skills/security-dast-sentinel/SKILL.md +1 -1
  24. package/.agents/skills/self-healing-locator/SKILL.md +1 -2
  25. package/.agents/skills/skillspector-security-guard/SKILL.md +115 -0
  26. package/.agents/skills/synthetic-test-data-generator/SKILL.md +1 -1
  27. package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
  28. package/.agents/skills/visual-regression-sentinel/SKILL.md +1 -1
  29. package/.agents/skills/wcag-audit-patterns/SKILL.md +1 -1
  30. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
  31. package/AGENTS.md +36 -0
  32. package/LICENSE +22 -0
  33. package/README.md +171 -107
  34. package/bin/agent-qa.js +4 -0
  35. package/package.json +3 -2
  36. package/skills_manifest.json +42 -29
  37. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +15 -1
  38. package/templates/ai_bridges/CLAUDE.md +31 -0
  39. package/templates/ai_bridges/copilot-instructions.md +25 -0
  40. package/templates/ai_bridges/cursor_synapse_qa.mdc +26 -0
  41. package/templates/ai_bridges/windsurfrules +25 -0
  42. package/templates/ci/github-actions-synapse.yml +28 -0
  43. package/templates/ci/gitlab-ci-synapse.yml +16 -0
  44. package/tools/TOOLS_RUNBOOK.md +223 -12
  45. package/tools/author_integrity.py +151 -0
  46. package/tools/browser_helpers.js +2 -2
  47. package/tools/bump_version.py +8 -0
  48. package/tools/cli.py +100 -4
  49. package/tools/export_postman_collection.py +535 -218
  50. package/tools/generate_jmx_plan.py +672 -102
  51. package/tools/git_pre_commit_guard.py +21 -0
  52. package/tools/jtl_analyzer.py +474 -0
  53. package/tools/mcp_excel_server.ps1 +1 -1
  54. package/tools/mcp_server.py +1 -1
  55. package/tools/newman_analyzer.py +385 -0
  56. package/tools/setup_project.py +203 -18
  57. package/tools/skillspector.py +1145 -0
  58. package/tools/telemetry_correlator.py +351 -0
  59. package/tools/templates/jmeter-distributed/README.md +54 -0
  60. package/tools/templates/jmeter-distributed/docker-compose.yml +83 -0
  61. package/tools/update_sentinel.py +313 -0
  62. package/tools/verify_all.py +30 -1
@@ -1,35 +1,43 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
3
  """
4
- tools/export_postman_collection.py — Enterprise API to Postman Collection v2.1.0 Exporter
5
- -----------------------------------------------------------------------------------------
6
- Converts OpenAPI / Swagger specifications, JSON endpoint schemas, or HTTP Archives (HAR)
7
- into a production-ready Postman Collection v2.1.0 (with Pre-request Scripts, Tests Scripts,
8
- Response Time Assertions, and Auto-Extracted Environment Variables).
9
-
10
- Features:
11
- 1. Ingests OpenAPI 3.0 / Swagger 2.0 JSON, HAR files, or built-in sample schemas.
12
- 2. Generates organized folders by Tags / Resources with parameterized routes.
13
- 3. Pre-request Scripts: Dynamic request timestamps, UUIDs, and automated auth headers.
4
+ tools/export_postman_collection.py — Enterprise API to Postman Collection v2.1.0 Exporter (Postman Pinnacle)
5
+ -----------------------------------------------------------------------------------------------------------
6
+ Converts OpenAPI / Swagger specifications, JSON endpoint schemas, HTTP Archives (HAR),
7
+ or Natural Language Domain archetypes into production-ready Postman Collections v2.1.0.
8
+
9
+ Pinnacle Enterprise Capabilities:
10
+ 1. Multi-Format Ingestion: OpenAPI 3.0 / Swagger 2.0, HAR traffic traces, or Zero-Input Domain Synthesis.
11
+ 2. Autonomous Zero-Input Synthesis: Generates complete multi-tier API collections from scratch
12
+ when no input files exist (`--domain [ecommerce|banking|saas]`).
13
+ 3. Pre-request Scripts: Dynamic request timestamps, UUIDs, nonces, and automated Bearer Auth headers.
14
14
  4. Test Scripts (pm.test):
15
15
  - Expected HTTP Status assertions.
16
- - Response time SLA latency assertions (< 1000ms).
16
+ - SLA Latency assertions (< 1000ms).
17
17
  - Response Content-Type JSON header validation.
18
- - Autonomous Token & ID Extractor: Auto-saves `token`, `access_token`, or `id` into
19
- `pm.environment` / `pm.collectionVariables` for chaining sequential API calls.
20
- 5. Companion Environment Generator: Outputs `postman_environment.json` with base URLs.
21
- 6. 100% UTF-8 No BOM, portable, Newman CLI and CI/CD compatible.
18
+ - Autonomous Token & ID Extractor (`pm.environment.set('bearerToken', ...)`).
19
+ - Native AJV JSON Schema Validation (`pm.response.to.have.jsonSchema(schema)`).
20
+ 5. Companion Environment Generator: Outputs `postman_environment.json` with base URLs and variables.
21
+ 6. Data-Driven Testing (DDT) Suite: Outputs companion `test_data.csv` for Newman matrix execution.
22
+ 7. 100% UTF-8 No BOM, portable, Newman CLI and CI/CD compatible.
22
23
 
23
24
  Usage:
24
- python tools/export_postman_collection.py --spec swagger.json -o collection.json
25
+ # 1. From existing OpenAPI / Swagger
26
+ python tools/export_postman_collection.py --spec swagger.json -o collection.json --env-output env.json
27
+
28
+ # 2. Zero-Input Autonomous Generation (No input files!)
29
+ python tools/export_postman_collection.py --domain ecommerce -o ecom_collection.json --ddt-output ecom_data.csv
30
+ python tools/export_postman_collection.py --domain banking -o banking_collection.json --env-output banking_env.json
31
+
32
+ # 3. From browser network trace (.har)
25
33
  python tools/export_postman_collection.py --har traffic.har -o collection.json
26
- python tools/export_postman_collection.py --sample -o sample_collection.json --env-output sample_env.json
27
34
  """
28
35
 
29
36
  import os
30
37
  import sys
31
38
  import json
32
39
  import uuid
40
+ import csv
33
41
  import argparse
34
42
  from typing import Dict, Any, List, Optional
35
43
  from urllib.parse import urlparse, parse_qs
@@ -50,10 +58,11 @@ def generate_uuid() -> str:
50
58
  def build_pre_request_script() -> List[str]:
51
59
  """Generates standard Postman Pre-request script lines."""
52
60
  return [
53
- "// Auto-generated Pre-request Script by Agent-QA Hub",
54
- "// 1. Dynamic Timestamp & Request ID generation",
61
+ "// Auto-generated Pre-request Script by Synapse-Hub",
62
+ "// 1. Dynamic Timestamp & Correlation ID generation",
55
63
  "pm.variables.set('req_timestamp', new Date().toISOString());",
56
64
  "pm.variables.set('req_nonce', Math.random().toString(36).substring(2, 12));",
65
+ "pm.variables.set('correlation_id', 'CORR-' + pm.variables.get('req_nonce'));",
57
66
  "",
58
67
  "// 2. Automated Bearer Token Injection if present in environment",
59
68
  "var token = pm.environment.get('bearerToken') || pm.collectionVariables.get('bearerToken');",
@@ -63,10 +72,14 @@ def build_pre_request_script() -> List[str]:
63
72
  ]
64
73
 
65
74
 
66
- def build_test_script(expected_status: int = 200, max_time_ms: int = 1000) -> List[str]:
67
- """Generates standard Postman Test script lines."""
68
- return [
69
- "// Auto-generated Test Assertions by Agent-QA Hub",
75
+ def build_test_script(
76
+ expected_status: int = 200,
77
+ max_time_ms: int = 1000,
78
+ response_schema: Optional[Dict[str, Any]] = None
79
+ ) -> List[str]:
80
+ """Generates comprehensive Postman Test script lines with AJV JSON Schema assertion."""
81
+ lines = [
82
+ "// Auto-generated Test Assertions by Synapse-Hub",
70
83
  f"// 1. Assert Status Code is {expected_status}",
71
84
  f"pm.test('Status code is {expected_status}', function () {{",
72
85
  f" pm.expect(pm.response.code).to.be.oneOf([{expected_status}, 200, 201]);",
@@ -93,12 +106,13 @@ def build_test_script(expected_status: int = 200, max_time_ms: int = 1000) -> Li
93
106
  " if (authToken) {",
94
107
  " pm.environment.set('bearerToken', authToken);",
95
108
  " pm.collectionVariables.set('bearerToken', authToken);",
96
- " console.log('[Agent-QA] Captured dynamic bearerToken: ' + authToken.substring(0, 15) + '...');",
109
+ " console.log('[Synapse-Hub] Captured dynamic bearerToken: ' + authToken.substring(0, 15) + '...');",
97
110
  " }",
98
111
  " // Auto-extract newly created ID",
99
112
  " var entityId = jsonBody.id || jsonBody.code || (jsonBody.data && (jsonBody.data.id || jsonBody.data.code));",
100
113
  " if (entityId) {",
101
114
  " pm.environment.set('lastCreatedId', String(entityId));",
115
+ " console.log('[Synapse-Hub] Captured lastCreatedId: ' + entityId);",
102
116
  " }",
103
117
  " } catch (e) {",
104
118
  " // Response is non-JSON or empty body",
@@ -106,6 +120,20 @@ def build_test_script(expected_status: int = 200, max_time_ms: int = 1000) -> Li
106
120
  "}"
107
121
  ]
108
122
 
123
+ # 5. AJV Native JSON Schema Validation
124
+ if response_schema:
125
+ schema_clean = json.dumps(response_schema, ensure_ascii=False)
126
+ lines.extend([
127
+ "",
128
+ "// 5. AJV JSON Schema Contract Validation",
129
+ "pm.test('Response conforms to OpenAPI JSON Schema', function () {",
130
+ f" var schema = {schema_clean};",
131
+ " pm.response.to.have.jsonSchema(schema);",
132
+ "});"
133
+ ])
134
+
135
+ return lines
136
+
109
137
 
110
138
  def mock_schema_payload(schema: Dict[str, Any], depth: int = 0) -> Any:
111
139
  """Recursively generates realistic mock JSON payload from OpenAPI schema."""
@@ -151,7 +179,6 @@ def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[st
151
179
  col_desc = info_dict.get("description", "Auto-generated Postman Collection from OpenAPI / Swagger")
152
180
 
153
181
  # Determine Base URL
154
- base_url = "{{baseUrl}}"
155
182
  if base_url_override:
156
183
  base_url_default = base_url_override
157
184
  elif "servers" in spec_data and spec_data["servers"]:
@@ -180,6 +207,11 @@ def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[st
180
207
  "key": "bearerToken",
181
208
  "value": "",
182
209
  "type": "string"
210
+ },
211
+ {
212
+ "key": "lastCreatedId",
213
+ "value": "1",
214
+ "type": "string"
183
215
  }
184
216
  ],
185
217
  "item": []
@@ -189,49 +221,45 @@ def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[st
189
221
  paths = spec_data.get("paths", {})
190
222
 
191
223
  for path, path_item in paths.items():
192
- for method in ("get", "post", "put", "patch", "delete", "options", "head"):
224
+ for method in ("get", "post", "put", "patch", "delete"):
193
225
  if method not in path_item:
194
226
  continue
195
227
 
196
228
  op = path_item[method]
197
229
  summary = op.get("summary") or f"{method.upper()} {path}"
198
- tags = op.get("tags") or ["Default"]
199
- tag_name = tags[0]
230
+ tags = op.get("tags", ["Default"])
231
+ tag_name = tags[0] if tags else "Default"
200
232
 
201
- # Request Headers
233
+ # Parse headers
202
234
  headers = [
203
235
  {"key": "Content-Type", "value": "application/json", "type": "text"},
204
236
  {"key": "Accept", "value": "application/json", "type": "text"}
205
237
  ]
206
238
 
207
- # Query Parameters
239
+ # Parse Query Parameters
208
240
  query_params = []
209
- for param in op.get("parameters", []):
210
- p_in = param.get("in")
211
- if p_in == "query":
241
+ for p in op.get("parameters", []):
242
+ if p.get("in") == "query":
212
243
  query_params.append({
213
- "key": param.get("name"),
214
- "value": str(param.get("example") or param.get("default", "")),
215
- "description": param.get("description", "")
244
+ "key": p.get("name"),
245
+ "value": str(p.get("example", "")),
246
+ "description": p.get("description", "")
216
247
  })
217
- elif p_in == "header":
248
+ elif p.get("in") == "header":
218
249
  headers.append({
219
- "key": param.get("name"),
220
- "value": str(param.get("example", "")),
221
- "type": "text",
222
- "description": param.get("description", "")
250
+ "key": p.get("name"),
251
+ "value": str(p.get("example", "")),
252
+ "type": "text"
223
253
  })
224
254
 
225
- # Request Body
255
+ # Parse Request Body
226
256
  body_dict = None
227
257
  if method in ("post", "put", "patch"):
228
- # OpenAPI 3.0 requestBody
229
258
  req_body = op.get("requestBody", {})
230
259
  content = req_body.get("content", {})
231
260
  json_content = content.get("application/json", {})
232
261
  schema = json_content.get("schema")
233
262
 
234
- # Swagger 2.0 body parameter fallback
235
263
  if not schema:
236
264
  for p in op.get("parameters", []):
237
265
  if p.get("in") == "body":
@@ -262,7 +290,7 @@ def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[st
262
290
  if query_params:
263
291
  url_obj["query"] = query_params
264
292
 
265
- # Status code for assertion
293
+ # Status code and response schema for assertion
266
294
  responses = op.get("responses", {})
267
295
  expected_status = 200
268
296
  for code_str in responses.keys():
@@ -270,6 +298,11 @@ def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[st
270
298
  expected_status = int(code_str)
271
299
  break
272
300
 
301
+ resp_info = responses.get(str(expected_status), {}) or responses.get("200", {}) or responses.get("201", {})
302
+ resp_schema = resp_info.get("content", {}).get("application/json", {}).get("schema")
303
+ if not resp_schema and "schema" in resp_info:
304
+ resp_schema = resp_info["schema"]
305
+
273
306
  postman_request = {
274
307
  "name": summary,
275
308
  "event": [
@@ -284,7 +317,10 @@ def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[st
284
317
  "listen": "test",
285
318
  "script": {
286
319
  "type": "text/javascript",
287
- "exec": build_test_script(expected_status=expected_status)
320
+ "exec": build_test_script(
321
+ expected_status=expected_status,
322
+ response_schema=resp_schema
323
+ )
288
324
  }
289
325
  }
290
326
  ],
@@ -304,11 +340,12 @@ def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[st
304
340
  folders[tag_name] = []
305
341
  folders[tag_name].append(postman_request)
306
342
 
307
- # Attach folders into collection
308
- for folder_name, items in folders.items():
343
+ # Nest grouped requests into folder items
344
+ for folder_name, req_list in folders.items():
309
345
  collection["item"].append({
310
346
  "name": folder_name,
311
- "item": items
347
+ "item": req_list,
348
+ "description": f"Endpoints grouped under {folder_name}"
312
349
  })
313
350
 
314
351
  return collection
@@ -317,38 +354,34 @@ def parse_openapi_spec(spec_data: Dict[str, Any], base_url_override: Optional[st
317
354
  def parse_har_file(har_data: Dict[str, Any], base_url_override: Optional[str] = None) -> Dict[str, Any]:
318
355
  """Parses HTTP Archive (.har) into a Postman Collection v2.1.0."""
319
356
  entries = har_data.get("log", {}).get("entries", [])
357
+ collection_name = "HAR Recorded API Collection"
358
+
359
+ base_url_default = base_url_override or "https://api.staging.enterprise.local"
360
+
320
361
  collection = {
321
362
  "info": {
322
363
  "_postman_id": generate_uuid(),
323
- "name": "HAR Session API Replay",
324
- "description": "Replay collection generated from production HAR network telemetry",
364
+ "name": collection_name,
365
+ "description": "Generated from HTTP Archive Network Trace",
325
366
  "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
326
367
  },
327
368
  "variable": [
328
- {"key": "baseUrl", "value": base_url_override or "https://api.staging.enterprise.local", "type": "string"},
329
- {"key": "bearerToken", "value": "", "type": "string"}
369
+ {"key": "baseUrl", "value": base_url_default, "type": "string"},
370
+ {"key": "bearerToken", "value": "", "type": "string"},
371
+ {"key": "lastCreatedId", "value": "1", "type": "string"}
330
372
  ],
331
373
  "item": []
332
374
  }
333
375
 
334
376
  requests_by_host: Dict[str, List[Dict[str, Any]]] = {}
335
377
 
336
- for entry in entries:
337
- req = entry.get("request", {})
338
- resp = entry.get("response", {})
339
- url_str = req.get("url", "")
378
+ for idx, e in enumerate(entries, 1):
379
+ req = e.get("request", {})
380
+ resp = e.get("response", {})
381
+ raw_url = req.get("url", "/")
340
382
  method = req.get("method", "GET").upper()
341
-
342
- parsed_url = urlparse(url_str)
343
- # Filter static files
344
- if any(parsed_url.path.lower().endswith(ext) for ext in (".png", ".jpg", ".css", ".js", ".woff2", ".ico")):
345
- continue
346
-
347
- host = parsed_url.netloc or "DefaultHost"
348
- path = parsed_url.path
349
- query = parse_qs(parsed_url.query)
350
-
351
- query_list = [{"key": k, "value": v[0] if v else ""} for k, v in query.items()]
383
+ p = urlparse(raw_url)
384
+ host = p.netloc or "Default"
352
385
 
353
386
  headers = []
354
387
  for h in req.get("headers", []):
@@ -356,19 +389,25 @@ def parse_har_file(har_data: Dict[str, Any], base_url_override: Optional[str] =
356
389
  if name.lower() not in ("cookie", "host", "content-length"):
357
390
  headers.append({"key": name, "value": h.get("value", ""), "type": "text"})
358
391
 
392
+ path_segments = [seg for seg in p.path.strip("/").split("/") if seg]
393
+ url_obj = {
394
+ "raw": f"{{{{baseUrl}}}}{p.path}" + (f"?{p.query}" if p.query else ""),
395
+ "host": ["{{baseUrl}}"],
396
+ "path": path_segments
397
+ }
398
+
359
399
  body_dict = None
360
- post_data = req.get("postData")
361
- if post_data and post_data.get("text"):
400
+ post_data = req.get("postData", {})
401
+ if post_data.get("text"):
362
402
  body_dict = {
363
403
  "mode": "raw",
364
- "raw": post_data.get("text"),
404
+ "raw": post_data["text"],
365
405
  "options": {"raw": {"language": "json"}}
366
406
  }
367
407
 
368
- status_code = resp.get("status", 200)
369
-
408
+ expected_status = resp.get("status", 200) or 200
370
409
  postman_item = {
371
- "name": f"{method} {path}",
410
+ "name": f"[{method}] {p.path or '/'}",
372
411
  "event": [
373
412
  {
374
413
  "listen": "prerequest",
@@ -376,21 +415,18 @@ def parse_har_file(har_data: Dict[str, Any], base_url_override: Optional[str] =
376
415
  },
377
416
  {
378
417
  "listen": "test",
379
- "script": {"type": "text/javascript", "exec": build_test_script(expected_status=status_code)}
418
+ "script": {"type": "text/javascript", "exec": build_test_script(expected_status=expected_status)}
380
419
  }
381
420
  ],
382
421
  "request": {
383
422
  "method": method,
384
423
  "header": headers,
385
- "url": {
386
- "raw": url_str,
387
- "host": [parsed_url.scheme + "://" + parsed_url.netloc],
388
- "path": [p for p in path.strip("/").split("/") if p],
389
- "query": query_list
390
- }
424
+ "url": url_obj,
425
+ "description": f"Recorded step {idx}"
391
426
  },
392
427
  "response": []
393
428
  }
429
+
394
430
  if body_dict:
395
431
  postman_item["request"]["body"] = body_dict
396
432
 
@@ -398,8 +434,12 @@ def parse_har_file(har_data: Dict[str, Any], base_url_override: Optional[str] =
398
434
  requests_by_host[host] = []
399
435
  requests_by_host[host].append(postman_item)
400
436
 
401
- for host_name, items in requests_by_host.items():
402
- collection["item"].append({"name": host_name, "item": items})
437
+ for host, reqs in requests_by_host.items():
438
+ collection["item"].append({
439
+ "name": f"Host: {host}",
440
+ "item": reqs,
441
+ "description": f"Recorded requests against {host}"
442
+ })
403
443
 
404
444
  return collection
405
445
 
@@ -410,53 +450,178 @@ def generate_postman_environment(name: str = "Enterprise-Staging-Env", base_url:
410
450
  "id": generate_uuid(),
411
451
  "name": name,
412
452
  "values": [
413
- {"key": "baseUrl", "value": base_url, "type": "default", "enabled": True},
414
- {"key": "bearerToken", "value": "", "type": "secret", "enabled": True},
415
- {"key": "lastCreatedId", "value": "", "type": "default", "enabled": True}
453
+ {"key": "baseUrl", "value": base_url, "enabled": True},
454
+ {"key": "bearerToken", "value": "", "enabled": True},
455
+ {"key": "lastCreatedId", "value": "1", "enabled": True},
456
+ {"key": "adminUser", "value": "qa_admin@enterprise.local", "enabled": True},
457
+ {"key": "adminPass", "value": "SecretVault#2026", "enabled": True}
416
458
  ],
417
459
  "_postman_variable_scope": "environment"
418
460
  }
419
461
 
420
462
 
421
- def get_sample_spec() -> Dict[str, Any]:
422
- """Provides standard built-in OpenAPI 3.0 spec for testing & demonstration."""
423
- return {
424
- "openapi": "3.0.0",
425
- "info": {
426
- "title": "Enterprise E-Commerce & Auth Service",
427
- "version": "1.0.0",
428
- "description": "Sample API spec for Postman & JMeter automated generation"
429
- },
430
- "servers": [{"url": "https://api.staging.enterprise.local"}],
431
- "paths": {
432
- "/api/v1/auth/login": {
433
- "post": {
434
- "tags": ["Authentication"],
435
- "summary": "Authenticate user and issue JWT token",
436
- "requestBody": {
437
- "content": {
438
- "application/json": {
439
- "schema": {
440
- "type": "object",
441
- "required": ["username", "password"],
442
- "properties": {
443
- "username": {"type": "string", "example": "admin_qa"},
444
- "password": {"type": "string", "example": "SecretPass#2026"}
463
+ def generate_ddt_dataset(domain: str = "ecommerce") -> List[Dict[str, Any]]:
464
+ """Generates companion Data-Driven Testing (DDT) matrix for Newman execution."""
465
+ if domain == "banking":
466
+ return [
467
+ {"scenario": "Valid Domestic Transfer", "sender": "ACC-101", "recipient": "ACC-902", "amount": 500000, "expectedStatus": 201},
468
+ {"scenario": "Zero Amount Boundary", "sender": "ACC-101", "recipient": "ACC-902", "amount": 0, "expectedStatus": 400},
469
+ {"scenario": "Negative Amount Attack", "sender": "ACC-101", "recipient": "ACC-902", "amount": -150000, "expectedStatus": 400},
470
+ {"scenario": "Exceed Daily Balance Limit", "sender": "ACC-101", "recipient": "ACC-902", "amount": 999999999, "expectedStatus": 422},
471
+ {"scenario": "Invalid Recipient Format", "sender": "ACC-101", "recipient": "<script>alert(1)</script>", "expectedStatus": 400}
472
+ ]
473
+ elif domain == "saas":
474
+ return [
475
+ {"scenario": "Standard Member Creation", "username": "member_valid", "role": "MEMBER", "priority": 10, "expectedStatus": 201},
476
+ {"scenario": "Admin Role Assignment", "username": "admin_chief", "role": "ADMIN", "priority": 1, "expectedStatus": 201},
477
+ {"scenario": "Empty Username Boundary", "username": "", "role": "MEMBER", "priority": 10, "expectedStatus": 400},
478
+ {"scenario": "SQLi Injection Probe", "username": "' OR '1'='1' --", "role": "MEMBER", "priority": 10, "expectedStatus": 400},
479
+ {"scenario": "Invalid Role Privilege", "username": "intruder_user", "role": "SUPER_ROOT_UNAUTHORIZED", "priority": 99, "expectedStatus": 403}
480
+ ]
481
+ else: # ecommerce default
482
+ return [
483
+ {"scenario": "Valid Catalog Creation", "code": "SKU_AUTO_2026", "name": "Enterprise Quantum Server", "priority": 10, "expectedStatus": 201},
484
+ {"scenario": "Short Code Boundary (< 3 chars)", "code": "A1", "name": "Invalid Item", "priority": 5, "expectedStatus": 400},
485
+ {"scenario": "Max Length Code Boundary", "code": "SKU_MAX_CHARS_990000", "name": "Max Length Product", "priority": 50, "expectedStatus": 201},
486
+ {"scenario": "Empty Product Name", "code": "SKU_VALID_02", "name": "", "priority": 20, "expectedStatus": 400},
487
+ {"scenario": "XSS Script Body Injection", "code": "SKU_XSS_03", "name": "<script>alert('pwned')</script>", "priority": 1, "expectedStatus": 400}
488
+ ]
489
+
490
+
491
+ def get_domain_spec(domain: str = "ecommerce") -> Dict[str, Any]:
492
+ """Autonomous Zero-Input Domain Spec Synthesizer."""
493
+ if domain == "banking":
494
+ return {
495
+ "openapi": "3.0.3",
496
+ "info": {
497
+ "title": "Enterprise Banking & Fund Transfer API",
498
+ "version": "2.4.0",
499
+ "description": "Production Banking API with OAuth2, Idempotent Transfers, and Audit Queries"
500
+ },
501
+ "servers": [{"url": "https://api.banking.staging.local"}],
502
+ "paths": {
503
+ "/api/v1/auth/token": {
504
+ "post": {
505
+ "tags": ["Authentication"],
506
+ "summary": "Obtain Bearer JWT Token",
507
+ "requestBody": {
508
+ "content": {
509
+ "application/json": {
510
+ "schema": {
511
+ "type": "object",
512
+ "required": ["clientId", "clientSecret"],
513
+ "properties": {
514
+ "clientId": {"type": "string", "example": "bank_admin"},
515
+ "clientSecret": {"type": "string", "example": "SecretVault#2026"}
516
+ }
517
+ }
518
+ }
519
+ }
520
+ },
521
+ "responses": {
522
+ "200": {
523
+ "description": "Token granted",
524
+ "content": {
525
+ "application/json": {
526
+ "schema": {
527
+ "type": "object",
528
+ "required": ["token", "tokenType", "expiresIn"],
529
+ "properties": {
530
+ "token": {"type": "string", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.fake"},
531
+ "tokenType": {"type": "string", "example": "Bearer"},
532
+ "expiresIn": {"type": "integer", "example": 3600}
533
+ }
534
+ }
445
535
  }
446
536
  }
447
537
  }
448
538
  }
449
- },
450
- "responses": {
451
- "200": {
452
- "description": "JWT Token generated successfully",
539
+ }
540
+ },
541
+ "/api/v1/accounts/me": {
542
+ "get": {
543
+ "tags": ["Account Management"],
544
+ "summary": "Get Account Balance & Profile",
545
+ "responses": {
546
+ "200": {
547
+ "description": "Account details retrieved",
548
+ "content": {
549
+ "application/json": {
550
+ "schema": {
551
+ "type": "object",
552
+ "required": ["accountNumber", "balance", "currency"],
553
+ "properties": {
554
+ "accountNumber": {"type": "string", "example": "ACC-889922"},
555
+ "balance": {"type": "number", "example": 25000000.0},
556
+ "currency": {"type": "string", "example": "VND"}
557
+ }
558
+ }
559
+ }
560
+ }
561
+ }
562
+ }
563
+ }
564
+ },
565
+ "/api/v1/transfers": {
566
+ "post": {
567
+ "tags": ["Fund Transfers"],
568
+ "summary": "Execute Idempotent Fund Transfer",
569
+ "requestBody": {
453
570
  "content": {
454
571
  "application/json": {
455
572
  "schema": {
456
573
  "type": "object",
574
+ "required": ["recipientAccount", "amount", "idempotencyKey"],
457
575
  "properties": {
458
- "token": {"type": "string"},
459
- "expires_in": {"type": "integer"}
576
+ "recipientAccount": {"type": "string", "example": "ACC-990011"},
577
+ "amount": {"type": "number", "example": 500000.0},
578
+ "idempotencyKey": {"type": "string", "example": "{{$guid}}"},
579
+ "remark": {"type": "string", "example": "Payment for invoice"}
580
+ }
581
+ }
582
+ }
583
+ }
584
+ },
585
+ "responses": {
586
+ "201": {
587
+ "description": "Transfer queued or completed",
588
+ "content": {
589
+ "application/json": {
590
+ "schema": {
591
+ "type": "object",
592
+ "required": ["id", "status", "transactionRef"],
593
+ "properties": {
594
+ "id": {"type": "string", "example": "TX-998822"},
595
+ "status": {"type": "string", "example": "COMPLETED"},
596
+ "transactionRef": {"type": "string", "example": "REF-AUTO-2026"}
597
+ }
598
+ }
599
+ }
600
+ }
601
+ }
602
+ }
603
+ }
604
+ },
605
+ "/api/v1/transfers/{id}": {
606
+ "get": {
607
+ "tags": ["Fund Transfers"],
608
+ "summary": "Audit Transfer Status by ID",
609
+ "parameters": [
610
+ {"name": "id", "in": "path", "required": True, "example": "{{lastCreatedId}}"}
611
+ ],
612
+ "responses": {
613
+ "200": {
614
+ "description": "Transfer status verified",
615
+ "content": {
616
+ "application/json": {
617
+ "schema": {
618
+ "type": "object",
619
+ "required": ["id", "status", "amount"],
620
+ "properties": {
621
+ "id": {"type": "string", "example": "TX-998822"},
622
+ "status": {"type": "string", "example": "COMPLETED"},
623
+ "amount": {"type": "number", "example": 500000.0}
624
+ }
460
625
  }
461
626
  }
462
627
  }
@@ -464,142 +629,294 @@ def get_sample_spec() -> Dict[str, Any]:
464
629
  }
465
630
  }
466
631
  }
632
+ }
633
+ }
634
+ elif domain == "saas":
635
+ return {
636
+ "openapi": "3.0.3",
637
+ "info": {
638
+ "title": "Enterprise SaaS Tenant & User RBAC API",
639
+ "version": "1.8.0",
640
+ "description": "Multi-Tenant SaaS Management Suite"
467
641
  },
468
- "/api/v1/categories": {
469
- "get": {
470
- "tags": ["Category Management"],
471
- "summary": "List categories with pagination",
472
- "parameters": [
473
- {"name": "page", "in": "query", "example": 1},
474
- {"name": "limit", "in": "query", "example": 20}
475
- ],
476
- "responses": {"200": {"description": "List of categories"}}
477
- },
478
- "post": {
479
- "tags": ["Category Management"],
480
- "summary": "Create a new category",
481
- "requestBody": {
482
- "content": {
483
- "application/json": {
484
- "schema": {
485
- "type": "object",
486
- "required": ["code", "name"],
487
- "properties": {
488
- "code": {"type": "string", "minLength": 3, "maxLength": 20},
489
- "name": {"type": "string", "minLength": 1, "maxLength": 100},
490
- "priority": {"type": "integer", "minimum": 1, "maximum": 100}
642
+ "servers": [{"url": "https://api.saas.staging.local"}],
643
+ "paths": {
644
+ "/api/v1/auth/login": {
645
+ "post": {
646
+ "tags": ["Identity"],
647
+ "summary": "Tenant Admin Login",
648
+ "requestBody": {
649
+ "content": {
650
+ "application/json": {
651
+ "schema": {
652
+ "type": "object",
653
+ "required": ["tenantId", "username", "password"],
654
+ "properties": {
655
+ "tenantId": {"type": "string", "example": "tenant_enterprise"},
656
+ "username": {"type": "string", "example": "admin"},
657
+ "password": {"type": "string", "example": "SecretPass#2026"}
658
+ }
659
+ }
660
+ }
661
+ }
662
+ },
663
+ "responses": {
664
+ "200": {
665
+ "description": "Login successful",
666
+ "content": {
667
+ "application/json": {
668
+ "schema": {
669
+ "type": "object",
670
+ "required": ["token", "role"],
671
+ "properties": {
672
+ "token": {"type": "string", "example": "jwt.tenant.token"},
673
+ "role": {"type": "string", "example": "TENANT_ADMIN"}
674
+ }
675
+ }
491
676
  }
492
677
  }
493
678
  }
494
679
  }
680
+ }
681
+ },
682
+ "/api/v1/users": {
683
+ "get": {
684
+ "tags": ["User Directory"],
685
+ "summary": "List Active Tenant Users",
686
+ "responses": {"200": {"description": "Users list"}}
495
687
  },
496
- "responses": {"201": {"description": "Category created successfully"}}
688
+ "post": {
689
+ "tags": ["User Directory"],
690
+ "summary": "Provision New Tenant Member",
691
+ "requestBody": {
692
+ "content": {
693
+ "application/json": {
694
+ "schema": {
695
+ "type": "object",
696
+ "required": ["email", "role"],
697
+ "properties": {
698
+ "email": {"type": "string", "example": "user_auto@tenant.com"},
699
+ "role": {"type": "string", "example": "MEMBER"}
700
+ }
701
+ }
702
+ }
703
+ }
704
+ },
705
+ "responses": {
706
+ "201": {
707
+ "description": "User created",
708
+ "content": {
709
+ "application/json": {
710
+ "schema": {
711
+ "type": "object",
712
+ "required": ["id", "email"],
713
+ "properties": {
714
+ "id": {"type": "string", "example": "USR-101"},
715
+ "email": {"type": "string", "example": "user_auto@tenant.com"}
716
+ }
717
+ }
718
+ }
719
+ }
720
+ }
721
+ }
722
+ }
723
+ },
724
+ "/api/v1/users/{id}": {
725
+ "delete": {
726
+ "tags": ["User Directory"],
727
+ "summary": "Deprovision Member (Teardown Cleanup)",
728
+ "parameters": [{"name": "id", "in": "path", "required": True, "example": "{{lastCreatedId}}"}],
729
+ "responses": {"204": {"description": "User deprovisioned"}}
730
+ }
497
731
  }
732
+ }
733
+ }
734
+ else: # E-commerce default
735
+ return {
736
+ "openapi": "3.0.3",
737
+ "info": {
738
+ "title": "Enterprise E-Commerce Core API",
739
+ "version": "3.2.0",
740
+ "description": "Enterprise E-Commerce Commerce Engine API with Auth, Search, Cart, Order, and Teardown"
498
741
  },
499
- "/api/v1/categories/{id}": {
500
- "delete": {
501
- "tags": ["Category Management"],
502
- "summary": "Delete category by ID (Idempotent cleanup)",
503
- "parameters": [
504
- {"name": "id", "in": "path", "required": True, "example": "{{lastCreatedId}}"}
505
- ],
506
- "responses": {"204": {"description": "Deleted"}}
742
+ "servers": [{"url": "https://api.staging.enterprise.local"}],
743
+ "paths": {
744
+ "/api/v1/auth/login": {
745
+ "post": {
746
+ "tags": ["Authentication"],
747
+ "summary": "User Authentication (Login)",
748
+ "requestBody": {
749
+ "content": {
750
+ "application/json": {
751
+ "schema": {
752
+ "type": "object",
753
+ "required": ["username", "password"],
754
+ "properties": {
755
+ "username": {"type": "string", "example": "admin"},
756
+ "password": {"type": "string", "example": "SecretVault#2026"}
757
+ }
758
+ }
759
+ }
760
+ }
761
+ },
762
+ "responses": {
763
+ "200": {
764
+ "description": "Successful Login",
765
+ "content": {
766
+ "application/json": {
767
+ "schema": {
768
+ "type": "object",
769
+ "required": ["token", "tokenType"],
770
+ "properties": {
771
+ "token": {"type": "string", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.fake"},
772
+ "tokenType": {"type": "string", "example": "Bearer"}
773
+ }
774
+ }
775
+ }
776
+ }
777
+ }
778
+ }
779
+ }
780
+ },
781
+ "/api/v1/catalogs": {
782
+ "get": {
783
+ "tags": ["Catalog Service"],
784
+ "summary": "Search Product Catalogs",
785
+ "parameters": [
786
+ {"name": "query", "in": "query", "example": "server"},
787
+ {"name": "limit", "in": "query", "example": "20"}
788
+ ],
789
+ "responses": {
790
+ "200": {
791
+ "description": "Catalogs search result",
792
+ "content": {
793
+ "application/json": {
794
+ "schema": {
795
+ "type": "object",
796
+ "required": ["items", "total"],
797
+ "properties": {
798
+ "items": {"type": "array", "items": {"type": "object"}},
799
+ "total": {"type": "integer", "example": 42}
800
+ }
801
+ }
802
+ }
803
+ }
804
+ }
805
+ }
806
+ },
807
+ "post": {
808
+ "tags": ["Catalog Service"],
809
+ "summary": "Create Product Item",
810
+ "requestBody": {
811
+ "content": {
812
+ "application/json": {
813
+ "schema": {
814
+ "type": "object",
815
+ "required": ["code", "name"],
816
+ "properties": {
817
+ "code": {"type": "string", "example": "SKU_AUTO_2026"},
818
+ "name": {"type": "string", "example": "Enterprise Quantum Server"},
819
+ "priority": {"type": "integer", "example": 10}
820
+ }
821
+ }
822
+ }
823
+ }
824
+ },
825
+ "responses": {
826
+ "201": {
827
+ "description": "Product created",
828
+ "content": {
829
+ "application/json": {
830
+ "schema": {
831
+ "type": "object",
832
+ "required": ["id", "code"],
833
+ "properties": {
834
+ "id": {"type": "string", "example": "PRD-998811"},
835
+ "code": {"type": "string", "example": "SKU_AUTO_2026"}
836
+ }
837
+ }
838
+ }
839
+ }
840
+ }
841
+ }
842
+ }
843
+ },
844
+ "/api/v1/catalogs/{id}": {
845
+ "delete": {
846
+ "tags": ["Catalog Service"],
847
+ "summary": "Delete Catalog Item (Idempotent Teardown)",
848
+ "parameters": [
849
+ {"name": "id", "in": "path", "required": True, "example": "{{lastCreatedId}}"}
850
+ ],
851
+ "responses": {"204": {"description": "Item deleted"}}
852
+ }
507
853
  }
508
854
  }
509
855
  }
510
- }
511
856
 
512
857
 
513
858
  def main():
514
859
  parser = argparse.ArgumentParser(
515
- description="Export OpenAPI or HAR to Postman Collection v2.1.0 with auto-asserts & token chaining."
860
+ description="Export OpenAPI, HAR, or Zero-Input Domain Synthesis to Postman Collection v2.1.0."
516
861
  )
517
862
  parser.add_argument("--spec", "-s", help="Path to OpenAPI / Swagger JSON specification file.")
518
863
  parser.add_argument("--har", help="Path to HTTP Archive (.har) recording file.")
864
+ parser.add_argument("--domain", choices=["ecommerce", "banking", "saas"], default="ecommerce", help="Zero-input domain synthesizer template. Default: ecommerce.")
519
865
  parser.add_argument("--base-url", "-b", help="Override base URL for {{baseUrl}} parameter.")
520
866
  parser.add_argument("--output", "-o", help="Target output Postman Collection file path (.json).")
521
867
  parser.add_argument("--env-output", help="Optional companion Postman Environment output file (.json).")
522
- parser.add_argument("--sample", action="store_true", help="Generate sample collection using built-in enterprise schema.")
523
- parser.add_argument("--portman", action="store_true", help="Delegate conversion to @apideck/portman via npx (contract-driven assertions).")
524
- parser.add_argument("--newman", action="store_true", help="Automatically trigger headless Newman execution on the generated collection.")
868
+ parser.add_argument("--ddt-output", help="Optional companion Data-Driven Testing (DDT) CSV file for Newman.")
869
+ parser.add_argument("--sample", action="store_true", help="Generate sample collection using domain synthesizer.")
525
870
 
526
871
  args = parser.parse_args()
527
872
 
528
- # If --portman is requested, run via npx @apideck/portman
529
- if args.portman:
530
- import subprocess
531
- import tempfile
532
- import shutil
533
-
534
- target_output = args.output or "postman_collection.json"
535
- spec_path = args.spec
536
-
537
- # Handle sample spec if no explicit spec file provided
538
- temp_spec_file = None
539
- if not spec_path or args.sample:
540
- fd, temp_spec_file = tempfile.mkstemp(suffix=".json", prefix="sample_spec_")
541
- with os.fdopen(fd, "w", encoding="utf-8") as f:
542
- json.dump(get_sample_spec(), f)
543
- spec_path = temp_spec_file
544
-
545
- portman_cmd = ["npx", "--yes", "@apideck/portman", "-l", spec_path, "-o", target_output]
546
- if args.base_url:
547
- portman_cmd.extend(["-b", args.base_url])
548
- if args.newman:
549
- portman_cmd.extend(["-n"])
550
-
551
- print(f"⚡ [PORTMAN BRIDGE] Executing: {' '.join(portman_cmd)}")
552
- try:
553
- res = subprocess.run(portman_cmd, check=True)
554
- print(f"✅ [PORTMAN SUCCESS] Collection generated via @apideck/portman: {target_output}")
555
- if temp_spec_file and os.path.exists(temp_spec_file):
556
- os.remove(temp_spec_file)
557
- return
558
- except (subprocess.CalledProcessError, FileNotFoundError) as e:
559
- print(f"⚠️ [PORTMAN FALLBACK] Could not run npx portman ({e}). Falling back to internal Pure-Python engine.")
560
- if temp_spec_file and os.path.exists(temp_spec_file):
561
- os.remove(temp_spec_file)
562
-
563
- if args.sample or (not args.spec and not args.har):
564
- spec_data = get_sample_spec()
565
- collection = parse_openapi_spec(spec_data, base_url_override=args.base_url)
566
- elif args.har:
567
- if not os.path.exists(args.har):
568
- print(f"❌ Error: HAR file not found: {args.har}", file=sys.stderr)
569
- sys.exit(1)
873
+ # Determine data source
874
+ if args.har and os.path.exists(args.har):
570
875
  with open(args.har, "r", encoding="utf-8") as f:
571
876
  har_data = json.load(f)
572
877
  collection = parse_har_file(har_data, base_url_override=args.base_url)
573
- else:
574
- if not os.path.exists(args.spec):
575
- print(f"❌ Error: OpenAPI specification file not found: {args.spec}", file=sys.stderr)
576
- sys.exit(1)
878
+ active_domain = "har"
879
+ elif args.spec and os.path.exists(args.spec):
577
880
  with open(args.spec, "r", encoding="utf-8") as f:
578
881
  spec_data = json.load(f)
579
882
  collection = parse_openapi_spec(spec_data, base_url_override=args.base_url)
883
+ active_domain = "spec"
884
+ else:
885
+ # Autonomous Zero-Input Synthesis (User provided no file!)
886
+ spec_data = get_domain_spec(args.domain)
887
+ collection = parse_openapi_spec(spec_data, base_url_override=args.base_url)
888
+ active_domain = args.domain
889
+ print(f"✨ [ZERO-INPUT SYNTHESIS] Synthesized complete '{args.domain.upper()}' Enterprise API Suite from scratch!")
580
890
 
581
- out_json = json.dumps(collection, indent=2, ensure_ascii=False)
891
+ target_output = args.output or f"{active_domain}_collection.json"
892
+ out_dir = os.path.dirname(os.path.abspath(target_output))
893
+ if out_dir and not os.path.exists(out_dir):
894
+ os.makedirs(out_dir, exist_ok=True)
582
895
 
583
- if args.output:
584
- out_dir = os.path.dirname(os.path.abspath(args.output))
585
- if out_dir and not os.path.exists(out_dir):
586
- os.makedirs(out_dir, exist_ok=True)
587
- with open(args.output, "w", encoding="utf-8") as f:
588
- f.write(out_json)
589
- print(f"✅ [POSTMAN EXPORT] Successfully created Postman Collection v2.1.0: {args.output}")
590
- else:
591
- print(out_json)
896
+ with open(target_output, "w", encoding="utf-8") as f:
897
+ json.dump(collection, f, indent=2, ensure_ascii=False)
898
+ print(f"✅ [POSTMAN EXPORT] Successfully created Postman Collection v2.1.0: {target_output}")
899
+ print(f"🛡️ Embedded: Pre-request Nonce & Auth | SLA Tests (< 1000ms) | AJV JSON Schema Validation | Token Chaining")
592
900
 
593
901
  # Optional Companion Environment
594
- if args.env_output:
595
- base_u = args.base_url or "https://api.staging.enterprise.local"
596
- env_dict = generate_postman_environment(name="Agent-QA-Staging-Env", base_url=base_u)
597
- env_dir = os.path.dirname(os.path.abspath(args.env_output))
598
- if env_dir and not os.path.exists(env_dir):
599
- os.makedirs(env_dir, exist_ok=True)
600
- with open(args.env_output, "w", encoding="utf-8") as f:
601
- json.dump(env_dict, f, indent=2, ensure_ascii=False)
602
- print(f"✅ [ENVIRONMENT EXPORT] Successfully created Postman Environment: {args.env_output}")
902
+ env_file = args.env_output or (os.path.splitext(target_output)[0] + "_env.json")
903
+ base_u = args.base_url or "https://api.staging.enterprise.local"
904
+ env_dict = generate_postman_environment(name=f"Synapse-{active_domain.capitalize()}-Env", base_url=base_u)
905
+ with open(env_file, "w", encoding="utf-8") as f:
906
+ json.dump(env_dict, f, indent=2, ensure_ascii=False)
907
+ print(f"🌐 [ENVIRONMENT EXPORT] Successfully created Postman Environment: {env_file}")
908
+
909
+ # Optional Companion Data-Driven Testing (DDT) CSV
910
+ if args.ddt_output or args.sample:
911
+ ddt_file = args.ddt_output or (os.path.splitext(target_output)[0] + "_ddt.csv")
912
+ dataset = generate_ddt_dataset(args.domain if active_domain in ("ecommerce", "banking", "saas") else "ecommerce")
913
+ if dataset:
914
+ with open(ddt_file, "w", encoding="utf-8", newline="") as f:
915
+ writer = csv.DictWriter(f, fieldnames=list(dataset[0].keys()))
916
+ writer.writeheader()
917
+ writer.writerows(dataset)
918
+ print(f"📊 [DDT EXPORT] Successfully created Data-Driven Testing CSV: {ddt_file}")
919
+ print(f"🚀 Execution Command: newman run \"{target_output}\" -e \"{env_file}\" -d \"{ddt_file}\"")
603
920
 
604
921
 
605
922
  if __name__ == "__main__":