synapse-hub 2.2.3 → 2.4.0

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 (317) hide show
  1. package/.agents/agents/failure-analyst.md +11 -0
  2. package/.agents/agents/live-explorer.md +28 -0
  3. package/.agents/agents/orchestrator.md +20 -4
  4. package/.agents/agents/pom-crafter.md +12 -0
  5. package/.agents/agents/quality-gatekeeper.md +19 -4
  6. package/.agents/agents/test-runner.md +11 -0
  7. package/.agents/rules/communication-style.md +40 -0
  8. package/.agents/rules/context-engineering.md +23 -0
  9. package/.agents/rules/git-push-protocol.md +11 -17
  10. package/.agents/rules/graphify.md +14 -14
  11. package/.agents/rules/lessons-learned.md +3 -3
  12. package/.agents/rules/mandatory-playwright-live.md +29 -29
  13. package/.agents/rules/mobile_appium_rules.md +132 -0
  14. package/.agents/rules/performance_testing_rules.md +74 -0
  15. package/.agents/rules/skill-precedence.md +1 -1
  16. package/.agents/rules/systematic-debugging.md +6 -6
  17. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
  18. package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
  19. package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
  20. package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
  21. package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
  22. package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
  23. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
  24. package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
  25. package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
  26. package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
  27. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +4 -1
  28. package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
  29. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
  30. package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
  31. package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
  32. package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
  33. package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
  34. package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
  35. package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
  36. package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
  37. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
  38. package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
  39. package/.agents/skills/code-refactoring-refactor-clean/resources/implementation-playbook.md +9 -7
  40. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
  41. package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
  42. package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
  43. package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
  44. package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
  45. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
  46. package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
  47. package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
  48. package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
  49. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
  50. package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
  51. package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
  52. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
  53. package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
  54. package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
  55. package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
  56. package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
  57. package/.agents/skills/github-actions-templates/SKILL.md +47 -304
  58. package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
  59. package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
  60. package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
  61. package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
  62. package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
  63. package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
  64. package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
  65. package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
  66. package/.agents/skills/lam-script-generator/SKILL.md +42 -1
  67. package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
  68. package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
  69. package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
  70. package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
  71. package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
  72. package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
  73. package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
  74. package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
  75. package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
  76. package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
  77. package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
  78. package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
  79. package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
  80. package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
  81. package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
  82. package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
  83. package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
  84. package/.agents/skills/security-dast-sentinel/SKILL.md +57 -135
  85. package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
  86. package/.agents/skills/self-healing-locator/SKILL.md +24 -10
  87. package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
  88. package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
  89. package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
  90. package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
  91. package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
  92. package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
  93. package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
  94. package/.agents/skills/systematic-debugging/SKILL.md +100 -0
  95. package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
  96. package/.agents/skills/test-driven-development/SKILL.md +114 -0
  97. package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
  98. package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
  99. package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
  100. package/.agents/skills/verification-before-completion/SKILL.md +100 -0
  101. package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
  102. package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
  103. package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
  104. package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
  105. package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
  106. package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
  107. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
  108. package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
  109. package/.agents/templates/completion_report.md +57 -57
  110. package/.agents/workflows/analyze_requirement_document.md +18 -0
  111. package/.agents/workflows/export_testcases_excel.md +32 -0
  112. package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
  113. package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
  114. package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
  115. package/.agents/workflows/generate_requirements_from_website.md +25 -0
  116. package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
  117. package/.agents/workflows/graphify.md +10 -10
  118. package/.agents/workflows/verify.md +2 -2
  119. package/.cursor/rules/language.mdc +7 -7
  120. package/.cursor/rules/synapse-qa.mdc +23 -0
  121. package/.cursorrules +7 -0
  122. package/.pre-commit-hooks.yaml +14 -0
  123. package/.windsurfrules +7 -0
  124. package/AGENTS.md +54 -15
  125. package/CLAUDE.md +35 -12
  126. package/LICENSE +30 -0
  127. package/NOTICE +180 -0
  128. package/README.en.md +325 -0
  129. package/README.md +247 -512
  130. package/action.yml +51 -0
  131. package/benchmarks/qa_eval/README.md +71 -0
  132. package/benchmarks/qa_eval/build_dataset.py +677 -0
  133. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
  134. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
  135. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
  136. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
  137. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
  138. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
  139. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
  140. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
  141. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
  142. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
  143. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
  144. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
  145. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
  146. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
  147. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
  148. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
  149. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
  150. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
  151. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
  152. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
  153. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
  154. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
  155. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
  156. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
  157. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
  158. package/benchmarks/qa_eval/eval_runner.py +418 -0
  159. package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
  160. package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
  161. package/benchmarks/qa_eval/reports/eval_results.json +105 -0
  162. package/bin/agent-qa.js +10 -3
  163. package/docs/ARCHITECTURE.md +107 -0
  164. package/docs/CI_CD_INTEGRATION.md +94 -0
  165. package/docs/CLI_REFERENCE.md +157 -0
  166. package/docs/CONTRIBUTING.md +91 -0
  167. package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
  168. package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
  169. package/docs/adopted-ecosystem-tools.md +87 -0
  170. package/docs/schemas/synapse.config.schema.json +64 -0
  171. package/examples/cypress-ecommerce-suite/README.md +45 -0
  172. package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
  173. package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
  174. package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
  175. package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
  176. package/examples/cypress-ecommerce-suite/package.json +13 -0
  177. package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
  178. package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
  179. package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
  180. package/examples/katalon-suite/Profiles/default.glbl +12 -0
  181. package/examples/katalon-suite/README.md +18 -0
  182. package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
  183. package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
  184. package/examples/katalon-suite/katalon_suite.prj +21 -0
  185. package/examples/playwright-crm-suite/README.md +40 -0
  186. package/examples/playwright-crm-suite/package.json +14 -0
  187. package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
  188. package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
  189. package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
  190. package/examples/playwright-crm-suite/playwright.config.ts +27 -0
  191. package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
  192. package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
  193. package/examples/plugins/sample-custom-plugin/README.md +14 -0
  194. package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
  195. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
  196. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
  197. package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
  198. package/examples/practices/README.md +22 -0
  199. package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
  200. package/examples/practices/requirements/crm/requirements_customers.md +462 -0
  201. package/examples/practices/requirements/crm/requirements_login.md +202 -0
  202. package/examples/practices/requirements/crm/requirements_projects.md +396 -0
  203. package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
  204. package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
  205. package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
  206. package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
  207. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
  208. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
  209. package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
  210. package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
  211. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
  212. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
  213. package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
  214. package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
  215. package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
  216. package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
  217. package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
  218. package/examples/pytest-api-suite/README.md +45 -0
  219. package/examples/pytest-api-suite/conftest.py +31 -0
  220. package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
  221. package/examples/pytest-api-suite/requirements.txt +2 -0
  222. package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
  223. package/examples/selenium-suite/README.md +18 -0
  224. package/examples/selenium-suite/pages/base_page.py +27 -0
  225. package/examples/selenium-suite/pages/login_page.py +29 -0
  226. package/examples/selenium-suite/pages/order_page.py +25 -0
  227. package/examples/selenium-suite/pytest.ini +6 -0
  228. package/examples/selenium-suite/requirements.txt +2 -0
  229. package/examples/selenium-suite/tests/test_login.py +10 -0
  230. package/examples/selenium-suite/tests/test_order.py +10 -0
  231. package/package.json +22 -7
  232. package/scripts/generate_skill_examples.py +244 -0
  233. package/scripts/install.ps1 +112 -0
  234. package/scripts/install_global_plugin.ps1 +94 -0
  235. package/scripts/install_global_plugin.sh +49 -0
  236. package/scripts/lint_skills.ps1 +57 -0
  237. package/scripts/package_dist.py +323 -0
  238. package/scripts/setup.sh +59 -0
  239. package/scripts/update_hub.ps1 +19 -0
  240. package/scripts/update_hub.sh +15 -0
  241. package/skills_manifest.json +209 -6
  242. package/synapse.config.json +30 -0
  243. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
  244. package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
  245. package/templates/ai_bridges/CLAUDE.md +33 -16
  246. package/templates/ai_bridges/copilot-instructions.md +14 -21
  247. package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
  248. package/templates/ai_bridges/windsurfrules +21 -21
  249. package/templates/ci/github-actions-synapse.yml +71 -4
  250. package/templates/welcome.html.tmpl +1674 -0
  251. package/tools/.infer_baseline.json +1102 -0
  252. package/tools/TOOLS_RUNBOOK.md +92 -130
  253. package/tools/__init__.py +26 -0
  254. package/tools/agent_canary.py +501 -501
  255. package/tools/analyze_project.py +31 -17
  256. package/tools/api_fuzzer.py +278 -6
  257. package/tools/author_integrity.py +6 -6
  258. package/tools/auto_heal.py +1 -1
  259. package/tools/browser_helpers.js +105 -7
  260. package/tools/bug_reporter.py +31 -1
  261. package/tools/bump_version.py +23 -3
  262. package/tools/cache_resolver.py +248 -14
  263. package/tools/check_env.py +55 -14
  264. package/tools/check_version_sync.py +153 -0
  265. package/tools/ci_healing_hook.py +1 -1
  266. package/tools/cli.py +263 -13
  267. package/tools/count_tokens.py +1257 -0
  268. package/tools/dashboard_generator.py +2638 -0
  269. package/tools/db_sandbox.py +198 -20
  270. package/tools/e2e_flow_prober.ps1 +12 -64
  271. package/tools/e2e_flow_prober.py +99 -0
  272. package/tools/e2e_flow_prober.sh +4 -0
  273. package/tools/export_postman_collection.py +29 -7
  274. package/tools/flaky_detector.py +4 -3
  275. package/tools/generate_allure_report.ps1 +12 -29
  276. package/tools/generate_allure_report.py +77 -0
  277. package/tools/generate_allure_report.sh +4 -0
  278. package/tools/generate_jmx_plan.py +299 -68
  279. package/tools/generate_test_reports.ps1 +12 -134
  280. package/tools/generate_test_reports.py +133 -0
  281. package/tools/generate_test_reports.sh +4 -0
  282. package/tools/gherkin_suite.py +758 -0
  283. package/tools/git_pre_commit_guard.py +12 -6
  284. package/tools/goldfish_review.py +366 -0
  285. package/tools/heal_pr_automator.py +1 -1
  286. package/tools/heuristic_locator_matcher.py +170 -21
  287. package/tools/infer_linter.py +612 -70
  288. package/tools/jtl_analyzer.py +1 -1
  289. package/tools/lint_ai_docs.py +64 -0
  290. package/tools/matrix_sharder.py +1 -1
  291. package/tools/mcp_excel_server.ps1 +12 -250
  292. package/tools/mcp_excel_server.py +249 -0
  293. package/tools/mcp_excel_server.sh +4 -0
  294. package/tools/mcp_server.py +368 -144
  295. package/tools/md_to_xlsx.py +307 -0
  296. package/tools/micro_mutator.py +300 -69
  297. package/tools/omni_transpiler.py +2 -2
  298. package/tools/output_formatter.py +128 -0
  299. package/tools/pinpoint_failure.py +88 -88
  300. package/tools/project_state.py +2 -2
  301. package/tools/repo_map.py +1 -1
  302. package/tools/safety_firewall.py +197 -33
  303. package/tools/setup_project.py +294 -30
  304. package/tools/skillspector.py +8 -8
  305. package/tools/spec_drift_sentinel.py +439 -0
  306. package/tools/sync_agent_rules.py +248 -0
  307. package/tools/telemetry_correlator.py +45 -0
  308. package/tools/test_impact.py +51 -12
  309. package/tools/tms_sync_bridge.py +21 -5
  310. package/tools/universal_ir.py +3 -0
  311. package/tools/update_sentinel.py +87 -15
  312. package/tools/verify_all.ps1 +13 -0
  313. package/tools/verify_all.py +463 -79
  314. package/tools/visual_diff_canvas.py +1 -0
  315. package/tools/visual_som_helper.py +1 -1
  316. package/tools/watch_mode.py +305 -0
  317. package/tools/welcome_page.py +291 -0
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enterprise Allure HTML Report Generator (Cross-Platform)
4
+ ---------------------------------------------------------
5
+ Dispatches Allure report generation and local web serving across Windows, macOS, and Linux.
6
+ Replaces legacy PowerShell-only scripts with a zero-dependency, cross-platform engine.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import shutil
12
+ import subprocess
13
+ import argparse
14
+
15
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
16
+ PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
17
+
18
+
19
+ def generate_allure_report(results_dir=None, report_dir=None, clean=True, open_browser=False):
20
+ """Generates an Allure HTML report from raw result files."""
21
+ results_path = results_dir or os.path.join(PROJECT_ROOT, "allure-results")
22
+ report_path = report_dir or os.path.join(PROJECT_ROOT, "allure-report")
23
+
24
+ if not os.path.exists(results_path):
25
+ print(f"[!] Results directory not found: {results_path}")
26
+ print("[!] Please execute your test suite first to generate raw test result files.")
27
+ return 0
28
+
29
+ allure_cmd = shutil.which("allure")
30
+ if not allure_cmd:
31
+ print("[!] Allure CLI executable was not found on PATH.")
32
+ print("[*] Quick installation instructions:")
33
+ print(" npm install -g allure-commandline")
34
+ print(" brew install allure (macOS)")
35
+ print(" scoop install allure (Windows)")
36
+ return 1
37
+
38
+ cmd = [allure_cmd, "generate", results_path, "-o", report_path]
39
+ if clean:
40
+ cmd.append("--clean")
41
+
42
+ print(f"[+] Generating Allure HTML report from '{results_path}' to '{report_path}'...")
43
+ try:
44
+ ret = subprocess.run(cmd, check=True)
45
+ print(f"[+] Report generated successfully at: {report_path}")
46
+ except subprocess.CalledProcessError as e:
47
+ print(f"[ERROR] Allure report generation failed with exit code {e.returncode}")
48
+ return e.returncode
49
+
50
+ if open_browser:
51
+ print("[+] Launching Allure report server...")
52
+ try:
53
+ subprocess.run([allure_cmd, "open", report_path])
54
+ except KeyboardInterrupt:
55
+ print("\n[+] Allure server closed.")
56
+
57
+ return 0
58
+
59
+
60
+ def main():
61
+ parser = argparse.ArgumentParser(description="Generate and serve Allure HTML test reports")
62
+ parser.add_argument("--results-dir", default=None, help="Directory containing raw allure-results")
63
+ parser.add_argument("--report-dir", default=None, help="Output directory for generated HTML report")
64
+ parser.add_argument("--no-clean", action="store_true", help="Do not clean output report directory before generation")
65
+ parser.add_argument("--open", action="store_true", help="Automatically open report in browser via local server")
66
+ args = parser.parse_args()
67
+
68
+ sys.exit(generate_allure_report(
69
+ results_dir=args.results_dir,
70
+ report_dir=args.report_dir,
71
+ clean=not args.no_clean,
72
+ open_browser=args.open
73
+ ))
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Enterprise Allure Report Generator Wrapper
3
+ set -e
4
+ python3 "$(dirname "$0")/generate_allure_report.py" "$@"
@@ -18,7 +18,11 @@ Comprehensive 10/10 Pinnacle Performance Features:
18
18
  - RegexExtractor for HTML hidden CSRF / Anti-Forgery tokens
19
19
  - JSONPostProcessor with Match Number 0 (Random dynamic item choice avoiding duplicate lockups)
20
20
  - Chaining Bearer JWT token & entity IDs.
21
- 6. Workload Archetypes: Linear, Step-Up (Break-point), Spike, and Soak profiles.
21
+ 6. Workload Archetypes: Linear, Step-Up (Break-point), Spike, and Soak profiles -
22
+ step/spike emit staged ThreadGroups (per-stage threads/ramp/hold/delay windows).
23
+ 6b. Locust Code-as-Code Export (`--emit-locust`): generates a runnable
24
+ locustfile.py (User class + weighted @task endpoints) from the same
25
+ OpenAPI/Postman/HAR ingestion - concept borrowed from Locust (MIT).
22
26
  7. Enterprise Security: JSR223 Groovy HMAC-SHA256 payload signer & correlation IDs.
23
27
  8. Full-Stack Observability & Telemetry:
24
28
  - InfluxDB Backend Listener for live Grafana APM dashboard streaming
@@ -57,6 +61,40 @@ if hasattr(sys.stdout, "reconfigure"):
57
61
  pass
58
62
 
59
63
 
64
+ def compute_workload_stages(workload_profile: str, vus: int, ramp_up: int, duration: int) -> List[Dict[str, int]]:
65
+ """Computes staged execution windows for step/spike profiles.
66
+
67
+ Each stage is a plain JMeter ThreadGroup with its own scheduler window
68
+ (threads, ramp, hold, delay) so staged load works on vanilla JMeter 5.x
69
+ without plugins. Linear/soak return a single stage (behavior unchanged).
70
+ """
71
+ if workload_profile == "step":
72
+ step_count = 4
73
+ per_step = max(1, (vus + step_count - 1) // step_count)
74
+ window_ramp = max(1, min(10, ramp_up))
75
+ window_hold = max(30, duration // step_count)
76
+ stages = []
77
+ for i in range(step_count):
78
+ threads = min(per_step * (i + 1), vus)
79
+ stages.append({"threads": threads, "ramp": window_ramp,
80
+ "hold": window_hold, "delay": i * (window_ramp + window_hold)})
81
+ return stages
82
+
83
+ if workload_profile == "spike":
84
+ base = max(1, round(vus * 0.2))
85
+ base_hold = max(10, round(duration * 0.7))
86
+ spike_hold = max(10, round(duration * 0.15))
87
+ tail_hold = max(10, duration - base_hold - spike_hold)
88
+ return [
89
+ {"threads": base, "ramp": ramp_up, "hold": base_hold, "delay": 0},
90
+ {"threads": vus, "ramp": 5, "hold": spike_hold, "delay": base_hold},
91
+ {"threads": base, "ramp": 5, "hold": tail_hold, "delay": base_hold + spike_hold},
92
+ ]
93
+
94
+ # linear / soak: single continuous stage (legacy behavior, property-overridable)
95
+ return [{"threads": vus, "ramp": ramp_up, "hold": duration, "delay": 0}]
96
+
97
+
60
98
  def build_jmx_xml(
61
99
  plan_name: str,
62
100
  base_host: str,
@@ -122,6 +160,16 @@ def build_jmx_xml(
122
160
  f' <stringProp name="Argument.value">{sla_p95}</stringProp>',
123
161
  ' <stringProp name="Argument.metadata">=</stringProp>',
124
162
  ' </elementProp>',
163
+ ' <elementProp name="MAX_LATENCY" elementType="Argument">',
164
+ ' <stringProp name="Argument.name">MAX_LATENCY</stringProp>',
165
+ f' <stringProp name="Argument.value">${{__P(max_latency, {max(3000, sla_p95 * 2)})}}</stringProp>',
166
+ ' <stringProp name="Argument.metadata">=</stringProp>',
167
+ ' </elementProp>',
168
+ ' <elementProp name="LAST_CREATED_ID" elementType="Argument">',
169
+ ' <stringProp name="Argument.name">LAST_CREATED_ID</stringProp>',
170
+ ' <stringProp name="Argument.value">1</stringProp>',
171
+ ' <stringProp name="Argument.metadata">=</stringProp>',
172
+ ' </elementProp>',
125
173
  ' <elementProp name="BEARER_TOKEN" elementType="Argument">',
126
174
  ' <stringProp name="Argument.name">BEARER_TOKEN</stringProp>',
127
175
  ' <stringProp name="Argument.value"></stringProp>',
@@ -341,23 +389,46 @@ def build_jmx_xml(
341
389
  if workload_profile == "soak" and duration < 3600:
342
390
  actual_duration = 3600 # Default 1h for soak
343
391
 
344
- xml_lines.extend([
345
- f' <!-- Main Thread Group: {workload_profile.upper()} Profile -->',
346
- f' <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="{tg_name}" enabled="true">',
347
- ' <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>',
348
- ' <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">',
349
- f' <boolProp name="LoopController.continue_forever">{continue_forever}</boolProp>',
350
- f' <stringProp name="LoopController.loops">${{__P(loops,{loops_str})}}</stringProp>',
351
- ' </elementProp>',
352
- f' <stringProp name="ThreadGroup.num_threads">${{__P(vus,{vus})}}</stringProp>',
353
- f' <stringProp name="ThreadGroup.ramp_time">${{__P(ramp_up,{ramp_up})}}</stringProp>',
354
- ' <boolProp name="ThreadGroup.scheduler">true</boolProp>',
355
- f' <stringProp name="ThreadGroup.duration">${{__P(duration,{actual_duration})}}</stringProp>',
356
- ' <stringProp name="ThreadGroup.delay">0</stringProp>',
357
- ' <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>',
358
- ' </ThreadGroup>',
359
- ' <hashTree>'
360
- ])
392
+ stages = compute_workload_stages(workload_profile, vus, ramp_up, actual_duration)
393
+ is_staged = len(stages) > 1
394
+
395
+ def _emit_stage_group(stage: Dict[str, int], stage_index: int) -> None:
396
+ """Emits one stage ThreadGroup header. Single-stage plans keep the
397
+ legacy property-overridable form (${__P(vus,...)}); staged plans use
398
+ concrete per-stage windows."""
399
+ name = tg_name if not is_staged else f"{tg_name} - Stage {stage_index + 1}"
400
+ if is_staged:
401
+ threads_prop = str(stage["threads"])
402
+ ramp_prop = str(stage["ramp"])
403
+ duration_prop = str(stage["hold"])
404
+ delay_prop = str(stage["delay"])
405
+ header = f' <!-- Workload Stage {stage_index + 1}/{len(stages)}: {stage["threads"]} VUs, ramp {stage["ramp"]}s, hold {stage["hold"]}s, delay {stage["delay"]}s -->'
406
+ else:
407
+ threads_prop = f'${{__P(vus,{stage["threads"]})}}'
408
+ ramp_prop = f'${{__P(ramp_up,{stage["ramp"]})}}'
409
+ duration_prop = f'${{__P(duration,{stage["hold"]})}}'
410
+ delay_prop = "0"
411
+ header = f' <!-- Main Thread Group: {workload_profile.upper()} Profile -->'
412
+ xml_lines.extend([
413
+ header,
414
+ f' <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="{name}" enabled="true">',
415
+ ' <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>',
416
+ ' <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">',
417
+ f' <boolProp name="LoopController.continue_forever">{continue_forever}</boolProp>',
418
+ f' <stringProp name="LoopController.loops">${{__P(loops,{loops_str})}}</stringProp>',
419
+ ' </elementProp>',
420
+ f' <stringProp name="ThreadGroup.num_threads">{threads_prop}</stringProp>',
421
+ f' <stringProp name="ThreadGroup.ramp_time">{ramp_prop}</stringProp>',
422
+ ' <boolProp name="ThreadGroup.scheduler">true</boolProp>',
423
+ f' <stringProp name="ThreadGroup.duration">{duration_prop}</stringProp>',
424
+ f' <stringProp name="ThreadGroup.delay">{delay_prop}</stringProp>',
425
+ ' <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>',
426
+ ' </ThreadGroup>',
427
+ ' <hashTree>'
428
+ ])
429
+
430
+ _emit_stage_group(stages[0], 0)
431
+ stage_body_start = len(xml_lines)
361
432
 
362
433
  # CSV Data Set Config (Dynamic credentials / entities)
363
434
  if csv_file:
@@ -469,9 +540,9 @@ def build_jmx_xml(
469
540
  f'{indent} <intProp name="Assertion.test_type">40</intProp>',
470
541
  f'{indent} </ResponseAssertion>',
471
542
  f'{indent} <hashTree/>',
472
- f'{indent} <!-- SLA Latency Assertion -->',
473
- f'{indent} <DurationAssertion guiclass="DurationAssertionGui" testclass="DurationAssertion" testname="Assert SLA Latency (&lt; ${{SLA_P95}}ms)" enabled="true">',
474
- f'{indent} <stringProp name="DurationAssertion.duration">${{SLA_P95}}</stringProp>',
543
+ f'{indent} <!-- SLA Ceiling Assertion (Percentile P95 is validated post-run by jtl_analyzer.py) -->',
544
+ f'{indent} <DurationAssertion guiclass="DurationAssertionGui" testclass="DurationAssertion" testname="Assert Max Latency Ceiling (&lt; ${{MAX_LATENCY}}ms)" enabled="true">',
545
+ f'{indent} <stringProp name="DurationAssertion.duration">${{MAX_LATENCY}}</stringProp>',
475
546
  f'{indent} </DurationAssertion>',
476
547
  f'{indent} <hashTree/>'
477
548
  ])
@@ -492,7 +563,7 @@ def build_jmx_xml(
492
563
  f'{indent} <!-- Deep Correlation: Random Array Item Selection (Match No: 0) -->',
493
564
  f'{indent} <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Random Item Selector" enabled="true">',
494
565
  f'{indent} <stringProp name="JSONPostProcessor.referenceNames">RANDOM_ITEM_ID</stringProp>',
495
- f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$..items[?(@.stock &gt; 0)].id, $..id, $..code</stringProp>',
566
+ f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$..id</stringProp>',
496
567
  f'{indent} <stringProp name="JSONPostProcessor.match_numbers">0</stringProp>',
497
568
  f'{indent} <stringProp name="JSONPostProcessor.defaultValues">${{RANDOM_ITEM_ID}}</stringProp>',
498
569
  f'{indent} </JSONPostProcessor>',
@@ -505,7 +576,7 @@ def build_jmx_xml(
505
576
  f'{indent} <!-- Auto-Extract Dynamic Bearer Token -->',
506
577
  f'{indent} <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Bearer Token" enabled="true">',
507
578
  f'{indent} <stringProp name="JSONPostProcessor.referenceNames">BEARER_TOKEN</stringProp>',
508
- f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$.token, $.access_token, $.data.token, $.data.access_token</stringProp>',
579
+ f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$..[\'token\',\'access_token\']</stringProp>',
509
580
  f'{indent} <stringProp name="JSONPostProcessor.match_numbers">1</stringProp>',
510
581
  f'{indent} <stringProp name="JSONPostProcessor.defaultValues">${{BEARER_TOKEN}}</stringProp>',
511
582
  f'{indent} </JSONPostProcessor>',
@@ -516,9 +587,9 @@ def build_jmx_xml(
516
587
  f'{indent} <!-- Auto-Extract Created Entity ID -->',
517
588
  f'{indent} <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="JSON Extractor - Entity ID" enabled="true">',
518
589
  f'{indent} <stringProp name="JSONPostProcessor.referenceNames">LAST_CREATED_ID</stringProp>',
519
- f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$.id, $.code, $.data.id, $.data.code</stringProp>',
590
+ f'{indent} <stringProp name="JSONPostProcessor.jsonPathExprs">$..[\'id\',\'code\']</stringProp>',
520
591
  f'{indent} <stringProp name="JSONPostProcessor.match_numbers">1</stringProp>',
521
- f'{indent} <stringProp name="JSONPostProcessor.defaultValues"></stringProp>',
592
+ f'{indent} <stringProp name="JSONPostProcessor.defaultValues">${{LAST_CREATED_ID}}</stringProp>',
522
593
  f'{indent} </JSONPostProcessor>',
523
594
  f'{indent} <hashTree/>'
524
595
  ])
@@ -667,47 +738,60 @@ def build_jmx_xml(
667
738
  ' <hashTree/>'
668
739
  ])
669
740
 
670
- # Result Collectors for CLI JTL export and Summary
741
+ # Capture the first stage's hashTree body, close it, then replicate it for
742
+ # the remaining staged profiles (step/spike) so every stage executes the
743
+ # full sampler set inside its own scheduler window (vanilla JMeter, no
744
+ # plugins). The Result Collector moves to Test-Plan level so all stages
745
+ # stream into one JTL file.
746
+ stage_body = xml_lines[stage_body_start:]
747
+ xml_lines.append(' </hashTree>')
748
+
749
+ for stage_idx in range(1, len(stages)):
750
+ _emit_stage_group(stages[stage_idx], stage_idx)
751
+ xml_lines.extend(stage_body)
752
+ xml_lines.append(' </hashTree>')
753
+
754
+ # Result Collectors for CLI JTL export and Summary (plan-level: one JTL
755
+ # across all workload stages)
671
756
  xml_lines.extend([
672
- ' <!-- Headless JTL Result Collector & Summary Report -->',
673
- ' <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">',
674
- ' <boolProp name="ResultCollector.error_logging">false</boolProp>',
675
- ' <objProp>',
676
- ' <name>saveConfig</name>',
677
- ' <value class="SampleSaveConfiguration">',
678
- ' <time>true</time>',
679
- ' <latency>true</latency>',
680
- ' <timestamp>true</timestamp>',
681
- ' <success>true</success>',
682
- ' <label>true</label>',
683
- ' <code>true</code>',
684
- ' <message>true</message>',
685
- ' <threadName>true</threadName>',
686
- ' <dataType>true</dataType>',
687
- ' <encoding>false</encoding>',
688
- ' <assertions>true</assertions>',
689
- ' <subresults>true</subresults>',
690
- ' <responseData>false</responseData>',
691
- ' <samplerData>false</samplerData>',
692
- ' <xml>false</xml>',
693
- ' <fieldNames>true</fieldNames>',
694
- ' <responseHeaders>false</responseHeaders>',
695
- ' <requestHeaders>false</requestHeaders>',
696
- ' <responseDataOnError>false</responseDataOnError>',
697
- ' <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>',
698
- ' <assertionsResultsToSave>0</assertionsResultsToSave>',
699
- ' <bytes>true</bytes>',
700
- ' <sentBytes>true</sentBytes>',
701
- ' <url>true</url>',
702
- ' <threadCounts>true</threadCounts>',
703
- ' <idleTime>true</idleTime>',
704
- ' <connectTime>true</connectTime>',
705
- ' </value>',
706
- ' </objProp>',
707
- ' <stringProp name="filename">jmeter_results.jtl</stringProp>',
708
- ' </ResultCollector>',
709
- ' <hashTree/>',
710
- ' </hashTree>',
757
+ ' <!-- Headless JTL Result Collector & Summary Report (plan-level, all stages) -->',
758
+ ' <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">',
759
+ ' <boolProp name="ResultCollector.error_logging">false</boolProp>',
760
+ ' <objProp>',
761
+ ' <name>saveConfig</name>',
762
+ ' <value class="SampleSaveConfiguration">',
763
+ ' <time>true</time>',
764
+ ' <latency>true</latency>',
765
+ ' <timestamp>true</timestamp>',
766
+ ' <success>true</success>',
767
+ ' <label>true</label>',
768
+ ' <code>true</code>',
769
+ ' <message>true</message>',
770
+ ' <threadName>true</threadName>',
771
+ ' <dataType>true</dataType>',
772
+ ' <encoding>false</encoding>',
773
+ ' <assertions>true</assertions>',
774
+ ' <subresults>true</subresults>',
775
+ ' <responseData>false</responseData>',
776
+ ' <samplerData>false</samplerData>',
777
+ ' <xml>false</xml>',
778
+ ' <fieldNames>true</fieldNames>',
779
+ ' <responseHeaders>false</responseHeaders>',
780
+ ' <requestHeaders>false</requestHeaders>',
781
+ ' <responseDataOnError>false</responseDataOnError>',
782
+ ' <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>',
783
+ ' <assertionsResultsToSave>0</assertionsResultsToSave>',
784
+ ' <bytes>true</bytes>',
785
+ ' <sentBytes>true</sentBytes>',
786
+ ' <url>true</url>',
787
+ ' <threadCounts>true</threadCounts>',
788
+ ' <idleTime>true</idleTime>',
789
+ ' <connectTime>true</connectTime>',
790
+ ' </value>',
791
+ ' </objProp>',
792
+ ' <stringProp name="filename">jmeter_results.jtl</stringProp>',
793
+ ' </ResultCollector>',
794
+ ' <hashTree/>',
711
795
  ' </hashTree>',
712
796
  ' </hashTree>',
713
797
  '</jmeterTestPlan>'
@@ -716,6 +800,108 @@ def build_jmx_xml(
716
800
  return "\n".join(xml_lines)
717
801
 
718
802
 
803
+ def generate_locustfile(
804
+ plan_name: str,
805
+ base_host: str,
806
+ protocol: str,
807
+ port: str,
808
+ samplers: List[Dict[str, Any]],
809
+ weighted_mix: Optional[str] = None,
810
+ think_time_min_ms: int = 0,
811
+ think_time_max_ms: int = 0,
812
+ ) -> str:
813
+ """Generates a runnable locustfile.py (code-as-code load model).
814
+
815
+ Concept borrowed from Locust (MIT): the plan becomes a User class with
816
+ weighted @task endpoints, honoring the same --weighted-mix traffic shaping
817
+ as the JMX export (weights chunk the samplers sequentially).
818
+ """
819
+ host = f"{protocol}://{base_host}" if port in ("80", "443") else f"{protocol}://{base_host}:{port}"
820
+
821
+ # Distribute weights: chunk samplers sequentially per percentage.
822
+ task_weights: List[int] = [1] * len(samplers)
823
+ if weighted_mix:
824
+ try:
825
+ parts = [p.strip() for p in weighted_mix.split(",")]
826
+ pcts = [int(p.split(":")[0]) for p in parts if p]
827
+ total = sum(pcts) or 1
828
+ chunk = max(1, (len(samplers) + len(pcts) - 1) // len(pcts))
829
+ for gi, pct in enumerate(pcts):
830
+ for si in range(gi * chunk, min((gi + 1) * chunk, len(samplers))):
831
+ task_weights[si] = max(1, round(pct * 10 / total))
832
+ except ValueError:
833
+ task_weights = [1] * len(samplers)
834
+
835
+ def _slug(name: str, idx: int) -> str:
836
+ words = re.sub(r"[^a-zA-Z0-9]+", "_", name).strip("_").lower() or f"endpoint_{idx}"
837
+ return f"task_{idx + 1:02d}_{words[:40]}"
838
+
839
+ def _locust_body(raw: str) -> str:
840
+ """Converts JMeter placeholders into a runtime-safe .format() body."""
841
+ if not raw:
842
+ return ""
843
+ s = raw.replace("${__time(HMS,)}", "\x01ts\x01")
844
+ s = s.replace("${RANDOM_ITEM_ID}", "\x01rnd\x01")
845
+ s = s.replace("${LAST_CREATED_ID}", "\x01last_id\x01")
846
+ s = re.sub(r"\$\{([A-Za-z0-9_]+)\}", lambda m: "sample_" + m.group(1).lower(), s)
847
+ s = s.replace("{", "{{").replace("}", "}}")
848
+ s = s.replace("\x01ts\x01", "{ts}").replace("\x01rnd\x01", "{rnd}").replace("\x01last_id\x01", "{last_id}")
849
+ return s
850
+
851
+ method_lines: List[str] = []
852
+ for idx, s in enumerate(samplers):
853
+ fn = _slug(s.get("name", f"{s.get('method')} {s.get('path')}"), idx)
854
+ weight = task_weights[idx] if idx < len(task_weights) else 1
855
+ method = (s.get("method") or "GET").upper()
856
+ path = (s.get("path") or "/").replace('"', '\\"')
857
+ method_lines.append(f" @task({weight})")
858
+ method_lines.append(f" def {fn}(self):")
859
+ method_lines.append(f' """{s.get("name", method + " " + path)}"""')
860
+ if method in ("POST", "PUT", "PATCH"):
861
+ method_lines.append(
862
+ f' self.client.{method.lower()}("{path}", '
863
+ f'name="{method} {path}", '
864
+ f'json=json.loads(BODIES["{fn}"].format(ts=datetime.now(timezone.utc).strftime("%H%M%S"), rnd=random.randint(1, 9999), last_id=LAST_CREATED_ID)))'
865
+ )
866
+ else:
867
+ method_lines.append(f' self.client.get("{path}", name="{method} {path}")')
868
+ method_lines.append("")
869
+
870
+ think_time = (
871
+ f"between({think_time_min_ms / 1000:.2f}, {think_time_max_ms / 1000:.2f})"
872
+ if think_time_max_ms > 0
873
+ else "constant_pacing(1.0)"
874
+ )
875
+
876
+ bodies = "\n".join(
877
+ f' "{_slug(s.get("name", ""), i)}": {_locust_body(s.get("body", ""))!r},'
878
+ for i, s in enumerate(samplers)
879
+ if (s.get("method") or "").upper() in ("POST", "PUT", "PATCH")
880
+ )
881
+
882
+ class_name = re.sub(r"[^A-Za-z0-9]+", "", plan_name.title()) or "EnterpriseUser"
883
+
884
+ return (
885
+ '"""Generated by Synapse Hub tools/generate_jmx_plan.py --emit-locust\n'
886
+ "Code-as-code load model (concept borrowed from Locust, MIT).\n"
887
+ "Run: locust -f this_file.py --host=HOST\n"
888
+ '"""\n'
889
+ "import json\n"
890
+ "import random\n"
891
+ "from datetime import datetime, timezone\n\n"
892
+ "from locust import HttpUser, task, between, constant_pacing\n\n"
893
+ "LAST_CREATED_ID = 1\n\n"
894
+ "BODIES = {\n"
895
+ f"{bodies}\n"
896
+ "}\n\n\n"
897
+ f"class {class_name}(HttpUser):\n"
898
+ f' """{plan_name}"""\n\n'
899
+ f" host = \"{host}\"\n"
900
+ f" wait_time = {think_time}\n\n"
901
+ + "\n".join(method_lines)
902
+ )
903
+
904
+
719
905
  def parse_openapi_for_jmeter(spec_data: Dict[str, Any]) -> tuple:
720
906
  """Extracts base configuration and samplers list from OpenAPI spec."""
721
907
  info_dict = spec_data.get("info", {})
@@ -752,7 +938,10 @@ def parse_openapi_for_jmeter(spec_data: Dict[str, Any]) -> tuple:
752
938
 
753
939
  def replace_path_param(match):
754
940
  pname = match.group(1)
755
- if pname.lower() in ("id", "categoryid", "entityid", "userid"):
941
+ plow = pname.lower()
942
+ if plow in ("itemid", "productid", "item_id", "product_id"):
943
+ return "${RANDOM_ITEM_ID}"
944
+ if plow in ("id", "categoryid", "entityid", "userid", "orderid", "order_id"):
756
945
  return "${LAST_CREATED_ID}"
757
946
  return f"${{{pname}}}"
758
947
 
@@ -794,6 +983,25 @@ def parse_openapi_for_jmeter(spec_data: Dict[str, Any]) -> tuple:
794
983
  "expected_status": expected_status
795
984
  })
796
985
 
986
+ def sampler_sort_key(s: Dict[str, Any]) -> int:
987
+ p = s.get("path", "").lower()
988
+ m = s.get("method", "GET")
989
+ if "auth" in p or "login" in p or "token" in p:
990
+ return 1
991
+ if m == "POST" and "{" not in s.get("path", "") and "${" not in s.get("path", ""):
992
+ return 2
993
+ if m == "GET" and "{" not in s.get("path", "") and "${" not in s.get("path", ""):
994
+ return 3
995
+ if m == "GET":
996
+ return 4
997
+ if m in ("PUT", "PATCH"):
998
+ return 5
999
+ if m == "DELETE":
1000
+ return 6
1001
+ return 7
1002
+
1003
+ samplers.sort(key=sampler_sort_key)
1004
+
797
1005
  return plan_name, base_host, protocol, port, samplers
798
1006
 
799
1007
 
@@ -897,6 +1105,7 @@ def main():
897
1105
  parser.add_argument("--loop-count", type=int, default=-1, help="Loop iterations count (-1 for infinite). Default: -1.")
898
1106
  parser.add_argument("--sla-p95", type=int, default=500, help="P95 Latency SLA assertion threshold in ms. Default: 500.")
899
1107
  parser.add_argument("--output", "-o", help="Target output JMeter test plan file path (.jmx).")
1108
+ parser.add_argument("--emit-locust", dest="emit_locust", help="Also/instead emit a runnable Locust locustfile.py (code-as-code load model).")
900
1109
  parser.add_argument("--sample", action="store_true", help="Generate a sample load test plan using built-in enterprise schema.")
901
1110
 
902
1111
  # Advanced JMeter 360° Extensions
@@ -993,8 +1202,8 @@ def main():
993
1202
  plan_name, base_host, protocol, port, samplers = parse_openapi_for_jmeter(spec_data)
994
1203
  else:
995
1204
  # Fallback to built-in sample spec
996
- from export_postman_collection import get_sample_spec
997
- spec_data = get_sample_spec()
1205
+ from export_postman_collection import get_domain_spec
1206
+ spec_data = get_domain_spec("ecommerce")
998
1207
  plan_name, base_host, protocol, port, samplers = parse_openapi_for_jmeter(spec_data)
999
1208
 
1000
1209
  # Base URL override if provided
@@ -1007,6 +1216,28 @@ def main():
1007
1216
  else:
1008
1217
  base_host = args.base_url
1009
1218
 
1219
+ # Code-as-code Locust export (concept borrowed from Locust, MIT)
1220
+ if args.emit_locust:
1221
+ locust_source = generate_locustfile(
1222
+ plan_name=plan_name,
1223
+ base_host=base_host,
1224
+ protocol=protocol,
1225
+ port=port,
1226
+ samplers=samplers,
1227
+ weighted_mix=args.weighted_mix,
1228
+ think_time_min_ms=args.think_time_min,
1229
+ think_time_max_ms=args.think_time_max,
1230
+ )
1231
+ out_dir = os.path.dirname(os.path.abspath(args.emit_locust))
1232
+ if out_dir and not os.path.exists(out_dir):
1233
+ os.makedirs(out_dir, exist_ok=True)
1234
+ with open(args.emit_locust, "w", encoding="utf-8") as f:
1235
+ f.write(locust_source)
1236
+ print(f"🐍 [LOCUST EXPORT] Runnable locustfile written: {args.emit_locust}")
1237
+ print(f" Run: locust -f \"{args.emit_locust}\" --host={protocol}://{base_host}:{port}")
1238
+ if not args.output:
1239
+ return
1240
+
1010
1241
  jmx_content = build_jmx_xml(
1011
1242
  plan_name=plan_name,
1012
1243
  base_host=base_host,