synapse-hub 2.2.4 → 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.
- package/.agents/agents/failure-analyst.md +11 -0
- package/.agents/agents/live-explorer.md +28 -0
- package/.agents/agents/orchestrator.md +20 -4
- package/.agents/agents/pom-crafter.md +12 -0
- package/.agents/agents/quality-gatekeeper.md +19 -4
- package/.agents/agents/test-runner.md +11 -0
- package/.agents/rules/communication-style.md +40 -0
- package/.agents/rules/context-engineering.md +23 -0
- package/.agents/rules/git-push-protocol.md +10 -16
- package/.agents/rules/graphify.md +14 -14
- package/.agents/rules/mandatory-playwright-live.md +29 -29
- package/.agents/rules/mobile_appium_rules.md +132 -0
- package/.agents/rules/performance_testing_rules.md +74 -0
- package/.agents/rules/systematic-debugging.md +6 -6
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
- package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
- package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
- package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
- package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
- package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
- package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
- package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
- package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
- package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
- package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
- package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
- package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
- package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
- package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
- package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
- package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
- package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
- package/.agents/skills/github-actions-templates/SKILL.md +47 -304
- package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
- package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
- package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
- package/.agents/skills/lam-script-generator/SKILL.md +42 -1
- package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
- package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
- package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
- package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
- package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
- package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
- package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
- package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
- package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
- package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
- package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
- package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +54 -135
- package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
- package/.agents/skills/self-healing-locator/SKILL.md +24 -10
- package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
- package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
- package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
- package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
- package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
- package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
- package/.agents/skills/systematic-debugging/SKILL.md +100 -0
- package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
- package/.agents/skills/test-driven-development/SKILL.md +114 -0
- package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
- package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
- package/.agents/skills/verification-before-completion/SKILL.md +100 -0
- package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
- package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
- package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
- package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
- package/.agents/templates/completion_report.md +57 -57
- package/.agents/workflows/analyze_requirement_document.md +18 -0
- package/.agents/workflows/export_testcases_excel.md +32 -0
- package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
- package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
- package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
- package/.agents/workflows/generate_requirements_from_website.md +25 -0
- package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
- package/.agents/workflows/graphify.md +10 -10
- package/.agents/workflows/verify.md +2 -2
- package/.cursor/rules/language.mdc +7 -7
- package/.cursor/rules/synapse-qa.mdc +23 -0
- package/.cursorrules +7 -0
- package/.pre-commit-hooks.yaml +14 -0
- package/.windsurfrules +7 -0
- package/AGENTS.md +54 -15
- package/CLAUDE.md +35 -12
- package/LICENSE +30 -0
- package/NOTICE +180 -0
- package/README.en.md +325 -0
- package/README.md +247 -516
- package/action.yml +51 -0
- package/benchmarks/qa_eval/README.md +71 -0
- package/benchmarks/qa_eval/build_dataset.py +677 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/eval_runner.py +418 -0
- package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
- package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
- package/benchmarks/qa_eval/reports/eval_results.json +105 -0
- package/bin/agent-qa.js +10 -3
- package/docs/ARCHITECTURE.md +107 -0
- package/docs/CI_CD_INTEGRATION.md +94 -0
- package/docs/CLI_REFERENCE.md +157 -0
- package/docs/CONTRIBUTING.md +91 -0
- package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
- package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
- package/docs/adopted-ecosystem-tools.md +87 -0
- package/docs/schemas/synapse.config.schema.json +64 -0
- package/examples/cypress-ecommerce-suite/README.md +45 -0
- package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
- package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
- package/examples/cypress-ecommerce-suite/package.json +13 -0
- package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
- package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
- package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
- package/examples/katalon-suite/Profiles/default.glbl +12 -0
- package/examples/katalon-suite/README.md +18 -0
- package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
- package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
- package/examples/katalon-suite/katalon_suite.prj +21 -0
- package/examples/playwright-crm-suite/README.md +40 -0
- package/examples/playwright-crm-suite/package.json +14 -0
- package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
- package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
- package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
- package/examples/playwright-crm-suite/playwright.config.ts +27 -0
- package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
- package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
- package/examples/plugins/sample-custom-plugin/README.md +14 -0
- package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
- package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
- package/examples/practices/README.md +22 -0
- package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
- package/examples/practices/requirements/crm/requirements_customers.md +462 -0
- package/examples/practices/requirements/crm/requirements_login.md +202 -0
- package/examples/practices/requirements/crm/requirements_projects.md +396 -0
- package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
- package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
- package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
- package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
- package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
- package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
- package/examples/pytest-api-suite/README.md +45 -0
- package/examples/pytest-api-suite/conftest.py +31 -0
- package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
- package/examples/pytest-api-suite/requirements.txt +2 -0
- package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
- package/examples/selenium-suite/README.md +18 -0
- package/examples/selenium-suite/pages/base_page.py +27 -0
- package/examples/selenium-suite/pages/login_page.py +29 -0
- package/examples/selenium-suite/pages/order_page.py +25 -0
- package/examples/selenium-suite/pytest.ini +6 -0
- package/examples/selenium-suite/requirements.txt +2 -0
- package/examples/selenium-suite/tests/test_login.py +10 -0
- package/examples/selenium-suite/tests/test_order.py +10 -0
- package/package.json +22 -7
- package/scripts/generate_skill_examples.py +244 -0
- package/scripts/install.ps1 +112 -0
- package/scripts/install_global_plugin.ps1 +94 -0
- package/scripts/install_global_plugin.sh +49 -0
- package/scripts/lint_skills.ps1 +57 -0
- package/scripts/package_dist.py +323 -0
- package/scripts/setup.sh +59 -0
- package/scripts/update_hub.ps1 +19 -0
- package/scripts/update_hub.sh +15 -0
- package/skills_manifest.json +209 -6
- package/synapse.config.json +30 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
- package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
- package/templates/ai_bridges/CLAUDE.md +33 -16
- package/templates/ai_bridges/copilot-instructions.md +14 -21
- package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
- package/templates/ai_bridges/windsurfrules +21 -21
- package/templates/ci/github-actions-synapse.yml +71 -4
- package/templates/welcome.html.tmpl +1674 -0
- package/tools/.infer_baseline.json +1102 -0
- package/tools/TOOLS_RUNBOOK.md +92 -130
- package/tools/__init__.py +26 -0
- package/tools/agent_canary.py +501 -501
- package/tools/analyze_project.py +31 -17
- package/tools/api_fuzzer.py +278 -6
- package/tools/author_integrity.py +6 -6
- package/tools/auto_heal.py +1 -1
- package/tools/browser_helpers.js +105 -7
- package/tools/bug_reporter.py +31 -1
- package/tools/bump_version.py +23 -3
- package/tools/cache_resolver.py +248 -14
- package/tools/check_env.py +55 -14
- package/tools/check_version_sync.py +153 -0
- package/tools/ci_healing_hook.py +1 -1
- package/tools/cli.py +263 -13
- package/tools/count_tokens.py +1257 -0
- package/tools/dashboard_generator.py +2638 -0
- package/tools/db_sandbox.py +198 -20
- package/tools/e2e_flow_prober.ps1 +12 -64
- package/tools/e2e_flow_prober.py +99 -0
- package/tools/e2e_flow_prober.sh +4 -0
- package/tools/flaky_detector.py +4 -3
- package/tools/generate_allure_report.ps1 +12 -29
- package/tools/generate_allure_report.py +77 -0
- package/tools/generate_allure_report.sh +4 -0
- package/tools/generate_jmx_plan.py +257 -58
- package/tools/generate_test_reports.ps1 +12 -134
- package/tools/generate_test_reports.py +133 -0
- package/tools/generate_test_reports.sh +4 -0
- package/tools/gherkin_suite.py +758 -0
- package/tools/git_pre_commit_guard.py +12 -6
- package/tools/goldfish_review.py +366 -0
- package/tools/heal_pr_automator.py +1 -1
- package/tools/heuristic_locator_matcher.py +170 -21
- package/tools/infer_linter.py +612 -70
- package/tools/lint_ai_docs.py +64 -0
- package/tools/matrix_sharder.py +1 -1
- package/tools/mcp_excel_server.ps1 +12 -250
- package/tools/mcp_excel_server.py +249 -0
- package/tools/mcp_excel_server.sh +4 -0
- package/tools/mcp_server.py +368 -144
- package/tools/md_to_xlsx.py +307 -0
- package/tools/micro_mutator.py +300 -69
- package/tools/omni_transpiler.py +2 -2
- package/tools/output_formatter.py +128 -0
- package/tools/pinpoint_failure.py +88 -88
- package/tools/project_state.py +2 -2
- package/tools/repo_map.py +1 -1
- package/tools/safety_firewall.py +196 -45
- package/tools/setup_project.py +294 -30
- package/tools/spec_drift_sentinel.py +439 -0
- package/tools/sync_agent_rules.py +248 -0
- package/tools/telemetry_correlator.py +45 -0
- package/tools/test_impact.py +51 -12
- package/tools/tms_sync_bridge.py +21 -5
- package/tools/universal_ir.py +3 -0
- package/tools/update_sentinel.py +87 -15
- package/tools/verify_all.ps1 +13 -0
- package/tools/verify_all.py +463 -79
- package/tools/visual_diff_canvas.py +1 -0
- package/tools/visual_som_helper.py +1 -1
- package/tools/watch_mode.py +305 -0
- package/tools/welcome_page.py +291 -0
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 Thế Vị (@TeeVee998). All rights reserved.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
# Project: Synapse-Hub (https://github.com/TeeVee998/Synapse-Hub)
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Gherkin BDD Suite Compiler - Natural-Language Test Ingestion (0-Token)
|
|
8
|
+
----------------------------------------------------------------------
|
|
9
|
+
Parses Gherkin `.feature` files (Feature / Rule / Background / Scenario /
|
|
10
|
+
Scenario Outline / Examples / Tags / Doc Strings / Data Tables) and compiles
|
|
11
|
+
them into:
|
|
12
|
+
|
|
13
|
+
1. An expanded, deterministic test-case matrix (one case per Example row).
|
|
14
|
+
2. Hub-native Universal IR (TestCaseIR + StepIR) for the omni transpile
|
|
15
|
+
pipeline (--emit-ir).
|
|
16
|
+
3. A Markdown BDD test plan with "proven vs owed" coverage-gap analysis
|
|
17
|
+
(missing assertions, missing negative paths, thin example tables,
|
|
18
|
+
unresolved parameters).
|
|
19
|
+
|
|
20
|
+
Provenance:
|
|
21
|
+
- Concept inspired by the Gherkin-driven E2E ingestion workflow popularized
|
|
22
|
+
by TestZeus Hercules (AGPL-3.0). This is an independent, from-scratch,
|
|
23
|
+
stdlib-only re-expression of the concept - no upstream code or text is
|
|
24
|
+
used (AGPL content must not be copied into this MIT repository).
|
|
25
|
+
- The "proven vs owed" coverage traceability idea is inspired by Kane CLI
|
|
26
|
+
(Apache-2.0), re-expressed in original wording.
|
|
27
|
+
- English Gherkin keywords only (Given/When/Then/And/But); a `# language:`
|
|
28
|
+
header is ignored by design.
|
|
29
|
+
|
|
30
|
+
Exit codes: 0 = compiled (warnings allowed), 1 = parse/authoring errors
|
|
31
|
+
(report and plan are still emitted so the suite can be iterated on).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
import json
|
|
38
|
+
import copy
|
|
39
|
+
import glob
|
|
40
|
+
import argparse
|
|
41
|
+
from dataclasses import dataclass, field
|
|
42
|
+
from datetime import datetime
|
|
43
|
+
from typing import Dict, List, Optional, Any
|
|
44
|
+
|
|
45
|
+
# Enforce UTF-8 stdout encoding for Windows console compatibility
|
|
46
|
+
if hasattr(sys.stdout, 'reconfigure'):
|
|
47
|
+
try:
|
|
48
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
49
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
54
|
+
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
55
|
+
if SCRIPT_DIR not in sys.path:
|
|
56
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
57
|
+
|
|
58
|
+
from universal_ir import ( # noqa: E402
|
|
59
|
+
ActionType,
|
|
60
|
+
AssertionType,
|
|
61
|
+
StepIR,
|
|
62
|
+
TestCaseIR,
|
|
63
|
+
WaitStrategy,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
REPORTS_DIR = os.path.join(REPO_ROOT, "reports")
|
|
67
|
+
OUTPUT_JSON = os.path.join(REPORTS_DIR, "gherkin_suite_report.json")
|
|
68
|
+
|
|
69
|
+
# ANSI Color codes
|
|
70
|
+
GREEN = "\033[92m"
|
|
71
|
+
RED = "\033[91m"
|
|
72
|
+
YELLOW = "\033[93m"
|
|
73
|
+
CYAN = "\033[96m"
|
|
74
|
+
BOLD = "\033[1m"
|
|
75
|
+
RESET = "\033[0m"
|
|
76
|
+
|
|
77
|
+
CONCRETE_STEP_KEYWORDS = ("Given", "When", "Then")
|
|
78
|
+
STEP_ROLES = {"Given": "arrange", "When": "act", "Then": "assert"}
|
|
79
|
+
NEGATIVE_SIGNAL_PATTERN = re.compile(
|
|
80
|
+
r"invalid|error|fail|negativ|empty|denied|unauthor|wrong|incorrect", re.IGNORECASE
|
|
81
|
+
)
|
|
82
|
+
QUOTE_PATTERN = re.compile(r'"([^"]+)"|\'([^\']+)\'')
|
|
83
|
+
PARAM_PATTERN = re.compile(r"<([^<>\n]+)>")
|
|
84
|
+
URL_PATTERN = re.compile(r"https?://[^\s\"']+")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Gherkin document model
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class GherkinStep:
|
|
93
|
+
keyword: str # Concrete keyword (And/But already resolved)
|
|
94
|
+
raw_keyword: str # Keyword as written in the file (may be And/But)
|
|
95
|
+
text: str
|
|
96
|
+
role: str # arrange | act | assert
|
|
97
|
+
docstring: Optional[str] = None
|
|
98
|
+
docstring_type: str = ""
|
|
99
|
+
table_rows: List[List[str]] = field(default_factory=list)
|
|
100
|
+
|
|
101
|
+
def display_text(self) -> str:
|
|
102
|
+
parts = [self.text]
|
|
103
|
+
if self.table_rows:
|
|
104
|
+
parts.append("\n".join("| " + " | ".join(r) + " |" for r in self.table_rows))
|
|
105
|
+
if self.docstring is not None:
|
|
106
|
+
parts.append('"""\n' + self.docstring + '\n"""')
|
|
107
|
+
return "\n".join(parts)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class GherkinExamples:
|
|
112
|
+
name: str = ""
|
|
113
|
+
tags: List[str] = field(default_factory=list)
|
|
114
|
+
header: Optional[List[str]] = None
|
|
115
|
+
rows: List[Dict[str, str]] = field(default_factory=list)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class GherkinScenario:
|
|
120
|
+
name: str
|
|
121
|
+
tags: List[str] = field(default_factory=list)
|
|
122
|
+
description: str = ""
|
|
123
|
+
steps: List[GherkinStep] = field(default_factory=list)
|
|
124
|
+
is_outline: bool = False
|
|
125
|
+
examples: List[GherkinExamples] = field(default_factory=list)
|
|
126
|
+
rule_name: str = ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class GherkinFeature:
|
|
131
|
+
file: str
|
|
132
|
+
name: str = ""
|
|
133
|
+
tags: List[str] = field(default_factory=list)
|
|
134
|
+
description: str = ""
|
|
135
|
+
background: List[GherkinStep] = field(default_factory=list)
|
|
136
|
+
scenarios: List[GherkinScenario] = field(default_factory=list)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Parser (line-based state machine, stdlib only)
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _split_table_row(line: str) -> List[str]:
|
|
144
|
+
"""Splits a Gherkin data-table row into cells, honoring \\| escapes."""
|
|
145
|
+
escaped = line.strip().replace("\\|", "\x00")
|
|
146
|
+
if escaped.startswith("|"):
|
|
147
|
+
escaped = escaped[1:]
|
|
148
|
+
if escaped.endswith("|"):
|
|
149
|
+
escaped = escaped[:-1]
|
|
150
|
+
return [cell.replace("\x00", "|").strip() for cell in escaped.split("|")]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _dedent_doc_line(line: str, indent: int) -> str:
|
|
154
|
+
"""Dedents a doc-string content line by the opening delimiter's indent
|
|
155
|
+
(per Gherkin spec); blank lines collapse to empty strings."""
|
|
156
|
+
if not line.strip():
|
|
157
|
+
return ""
|
|
158
|
+
if len(line) >= indent and line[:indent].strip() == "":
|
|
159
|
+
return line[indent:]
|
|
160
|
+
return line.lstrip()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class GherkinParser:
|
|
164
|
+
"""Parses English Gherkin text into a GherkinFeature model."""
|
|
165
|
+
|
|
166
|
+
def __init__(self):
|
|
167
|
+
self.errors: List[str] = []
|
|
168
|
+
|
|
169
|
+
def _err(self, message: str) -> None:
|
|
170
|
+
self.errors.append(message)
|
|
171
|
+
|
|
172
|
+
def parse(self, text: str, filename: str = "<memory>") -> GherkinFeature:
|
|
173
|
+
feature = GherkinFeature(file=filename)
|
|
174
|
+
pending_tags: List[str] = []
|
|
175
|
+
rule_tags: List[str] = []
|
|
176
|
+
rule_name = ""
|
|
177
|
+
container: Optional[str] = None # None | "background" | "scenario"
|
|
178
|
+
current_scenario: Optional[GherkinScenario] = None
|
|
179
|
+
current_examples: Optional[GherkinExamples] = None
|
|
180
|
+
examples_mode = False
|
|
181
|
+
last_step: Optional[GherkinStep] = None
|
|
182
|
+
last_concrete: Optional[str] = None
|
|
183
|
+
in_docstring = False
|
|
184
|
+
doc_delim = '"""'
|
|
185
|
+
doc_type = ""
|
|
186
|
+
doc_indent = 0
|
|
187
|
+
doc_lines: List[str] = []
|
|
188
|
+
|
|
189
|
+
lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
|
190
|
+
for line_no, raw in enumerate(lines, 1):
|
|
191
|
+
if in_docstring:
|
|
192
|
+
if raw.strip() == doc_delim:
|
|
193
|
+
if last_step is not None:
|
|
194
|
+
last_step.docstring = "\n".join(doc_lines)
|
|
195
|
+
last_step.docstring_type = doc_type
|
|
196
|
+
else:
|
|
197
|
+
self._err(f"line {line_no}: doc string closed but no step is open")
|
|
198
|
+
in_docstring = False
|
|
199
|
+
doc_lines = []
|
|
200
|
+
doc_type = ""
|
|
201
|
+
else:
|
|
202
|
+
doc_lines.append(_dedent_doc_line(raw, doc_indent))
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
stripped = raw.strip()
|
|
206
|
+
if not stripped or stripped.startswith("#"):
|
|
207
|
+
continue # blank line or comment (incl. `# language:` header)
|
|
208
|
+
|
|
209
|
+
if stripped.startswith("@"):
|
|
210
|
+
pending_tags.extend(stripped.split())
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
# Doc string openers attach to the most recent step.
|
|
214
|
+
if stripped.startswith('"""') or stripped.startswith("```"):
|
|
215
|
+
if last_step is None or examples_mode:
|
|
216
|
+
self._err(f"line {line_no}: doc string outside a step")
|
|
217
|
+
else:
|
|
218
|
+
in_docstring = True
|
|
219
|
+
doc_delim = stripped[:3]
|
|
220
|
+
doc_type = stripped[3:].strip()
|
|
221
|
+
doc_indent = len(raw) - len(raw.lstrip())
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
m = re.match(r"Feature\s*:\s*(.*)", stripped)
|
|
225
|
+
if m:
|
|
226
|
+
feature.name = m.group(1).strip()
|
|
227
|
+
feature.tags = pending_tags
|
|
228
|
+
pending_tags = []
|
|
229
|
+
container = None
|
|
230
|
+
examples_mode = False
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
m = re.match(r"Rule\s*:\s*(.*)", stripped)
|
|
234
|
+
if m:
|
|
235
|
+
rule_name = m.group(1).strip()
|
|
236
|
+
rule_tags = pending_tags
|
|
237
|
+
pending_tags = []
|
|
238
|
+
container = None
|
|
239
|
+
examples_mode = False
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
m = re.match(r"Background\s*:\s*(.*)", stripped)
|
|
243
|
+
if m:
|
|
244
|
+
container = "background"
|
|
245
|
+
examples_mode = False
|
|
246
|
+
last_step = None
|
|
247
|
+
last_concrete = None
|
|
248
|
+
pending_tags = [] # Background tags are not modeled
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
m = re.match(r"Scenario\s+(?:Outline|Template)\s*:\s*(.*)", stripped)
|
|
252
|
+
if m:
|
|
253
|
+
container = "scenario"
|
|
254
|
+
examples_mode = False
|
|
255
|
+
last_step = None
|
|
256
|
+
last_concrete = None
|
|
257
|
+
current_scenario = GherkinScenario(
|
|
258
|
+
name=m.group(1).strip(),
|
|
259
|
+
tags=feature.tags + rule_tags + pending_tags,
|
|
260
|
+
is_outline=True,
|
|
261
|
+
rule_name=rule_name,
|
|
262
|
+
)
|
|
263
|
+
feature.scenarios.append(current_scenario)
|
|
264
|
+
pending_tags = []
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
m = re.match(r"Scenario\s*:\s*(.*)", stripped)
|
|
268
|
+
if m:
|
|
269
|
+
container = "scenario"
|
|
270
|
+
examples_mode = False
|
|
271
|
+
last_step = None
|
|
272
|
+
last_concrete = None
|
|
273
|
+
current_scenario = GherkinScenario(
|
|
274
|
+
name=m.group(1).strip(),
|
|
275
|
+
tags=feature.tags + rule_tags + pending_tags,
|
|
276
|
+
rule_name=rule_name,
|
|
277
|
+
)
|
|
278
|
+
feature.scenarios.append(current_scenario)
|
|
279
|
+
pending_tags = []
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
m = re.match(r"Examples\s*:\s*(.*)", stripped)
|
|
283
|
+
if m:
|
|
284
|
+
if current_scenario is None or not current_scenario.is_outline:
|
|
285
|
+
self._err(f"line {line_no}: Examples block without a Scenario Outline")
|
|
286
|
+
else:
|
|
287
|
+
current_examples = GherkinExamples(name=m.group(1).strip(), tags=pending_tags)
|
|
288
|
+
current_scenario.examples.append(current_examples)
|
|
289
|
+
examples_mode = True
|
|
290
|
+
pending_tags = []
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
m = re.match(r"(Given|When|Then|And|But)\s+(.*)", stripped)
|
|
294
|
+
if m:
|
|
295
|
+
raw_kw, step_text = m.group(1), m.group(2).strip()
|
|
296
|
+
if raw_kw in CONCRETE_STEP_KEYWORDS:
|
|
297
|
+
concrete = raw_kw
|
|
298
|
+
role = STEP_ROLES[concrete]
|
|
299
|
+
else:
|
|
300
|
+
if last_concrete is None:
|
|
301
|
+
self._err(f"line {line_no}: '{raw_kw}' before any Given/When/Then")
|
|
302
|
+
concrete = "Given"
|
|
303
|
+
role = "arrange"
|
|
304
|
+
else:
|
|
305
|
+
concrete = last_concrete
|
|
306
|
+
role = STEP_ROLES[concrete]
|
|
307
|
+
step = GherkinStep(keyword=concrete, raw_keyword=raw_kw, text=step_text, role=role)
|
|
308
|
+
if container == "background":
|
|
309
|
+
feature.background.append(step)
|
|
310
|
+
elif container == "scenario" and current_scenario is not None:
|
|
311
|
+
current_scenario.steps.append(step)
|
|
312
|
+
else:
|
|
313
|
+
self._err(f"line {line_no}: step outside Scenario/Background - '{stripped}'")
|
|
314
|
+
last_step = step
|
|
315
|
+
last_concrete = concrete
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
if stripped.startswith("|"):
|
|
319
|
+
cells = _split_table_row(stripped)
|
|
320
|
+
if examples_mode and current_examples is not None:
|
|
321
|
+
if current_examples.header is None:
|
|
322
|
+
current_examples.header = cells
|
|
323
|
+
else:
|
|
324
|
+
if len(cells) != len(current_examples.header):
|
|
325
|
+
self._err(
|
|
326
|
+
f"line {line_no}: example row has {len(cells)} cells, "
|
|
327
|
+
f"header has {len(current_examples.header)}"
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
current_examples.rows.append(dict(zip(current_examples.header, cells)))
|
|
331
|
+
elif last_step is not None:
|
|
332
|
+
last_step.table_rows.append(cells)
|
|
333
|
+
else:
|
|
334
|
+
self._err(f"line {line_no}: data table outside a step or Examples block")
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
# Prose: attach to the current container description.
|
|
338
|
+
if container == "scenario" and current_scenario is not None:
|
|
339
|
+
current_scenario.description = (current_scenario.description + " " + stripped).strip()
|
|
340
|
+
elif container is None:
|
|
341
|
+
feature.description = (feature.description + " " + stripped).strip()
|
|
342
|
+
|
|
343
|
+
if in_docstring:
|
|
344
|
+
self._err("unterminated doc string at end of file")
|
|
345
|
+
|
|
346
|
+
return feature
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
# Scenario Outline expansion
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
def _substitute_param(text: str, row: Dict[str, str]) -> str:
|
|
354
|
+
for key, value in row.items():
|
|
355
|
+
text = text.replace("<" + key + ">", value)
|
|
356
|
+
return text
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _slug(text: str) -> str:
|
|
360
|
+
slug = re.sub(r"[^a-zA-Z0-9]+", "-", text.strip().lower()).strip("-")
|
|
361
|
+
return slug or "unnamed"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def expand_feature(feature: GherkinFeature, errors: List[str]) -> List[Dict[str, Any]]:
|
|
365
|
+
"""Expands Scenario Outlines into concrete cases (one per Examples row)."""
|
|
366
|
+
seen_ids: Dict[str, int] = {}
|
|
367
|
+
cases: List[Dict[str, Any]] = []
|
|
368
|
+
|
|
369
|
+
def register(case_id: str) -> str:
|
|
370
|
+
if case_id in seen_ids:
|
|
371
|
+
seen_ids[case_id] += 1
|
|
372
|
+
return f"{case_id}-{seen_ids[case_id]}"
|
|
373
|
+
seen_ids[case_id] = 1
|
|
374
|
+
return case_id
|
|
375
|
+
|
|
376
|
+
for scenario in feature.scenarios:
|
|
377
|
+
base_steps = feature.background + scenario.steps
|
|
378
|
+
if not scenario.is_outline:
|
|
379
|
+
cases.append({
|
|
380
|
+
"id": register(f"{_slug(feature.name)}--{_slug(scenario.name)}"),
|
|
381
|
+
"name": scenario.name,
|
|
382
|
+
"tags": scenario.tags,
|
|
383
|
+
"steps": copy.deepcopy(base_steps),
|
|
384
|
+
"scenario": scenario.name,
|
|
385
|
+
"row": None,
|
|
386
|
+
"errors": [],
|
|
387
|
+
})
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
if not scenario.examples:
|
|
391
|
+
errors.append(f"Scenario Outline '{scenario.name}' has no Examples block")
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
for example_block in scenario.examples:
|
|
395
|
+
if example_block.header is None:
|
|
396
|
+
errors.append(f"Scenario Outline '{scenario.name}' has an Examples block without a table")
|
|
397
|
+
continue
|
|
398
|
+
for row_no, row in enumerate(example_block.rows, 1):
|
|
399
|
+
case_errors: List[str] = []
|
|
400
|
+
expanded: List[GherkinStep] = []
|
|
401
|
+
for step in base_steps:
|
|
402
|
+
text = _substitute_param(step.text, row)
|
|
403
|
+
doc = _substitute_param(step.docstring, row) if step.docstring is not None else None
|
|
404
|
+
table = [
|
|
405
|
+
[_substitute_param(cell, row) for cell in row_cells]
|
|
406
|
+
for row_cells in step.table_rows
|
|
407
|
+
]
|
|
408
|
+
leftover = PARAM_PATTERN.search(text)
|
|
409
|
+
if leftover:
|
|
410
|
+
case_errors.append(
|
|
411
|
+
f"unresolved parameter <{leftover.group(1)}> in step '{step.text}' "
|
|
412
|
+
f"(not found in Examples header)"
|
|
413
|
+
)
|
|
414
|
+
expanded.append(GherkinStep(
|
|
415
|
+
keyword=step.keyword,
|
|
416
|
+
raw_keyword=step.raw_keyword,
|
|
417
|
+
text=text,
|
|
418
|
+
role=step.role,
|
|
419
|
+
docstring=doc,
|
|
420
|
+
table_rows=table,
|
|
421
|
+
))
|
|
422
|
+
row_tag = f"@row-{row_no}"
|
|
423
|
+
cases.append({
|
|
424
|
+
"id": register(f"{_slug(feature.name)}--{_slug(scenario.name)}--row-{row_no}"),
|
|
425
|
+
"name": f"{scenario.name} [{row_tag}]",
|
|
426
|
+
"tags": scenario.tags + [row_tag],
|
|
427
|
+
"steps": expanded,
|
|
428
|
+
"scenario": scenario.name,
|
|
429
|
+
"row": row_no,
|
|
430
|
+
"errors": case_errors,
|
|
431
|
+
})
|
|
432
|
+
return cases
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
# Deterministic NL -> Universal IR mapping (keyword heuristics, 0 LLM tokens)
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
def _first_quoted(text: str) -> Optional[str]:
|
|
440
|
+
m = QUOTE_PATTERN.search(text)
|
|
441
|
+
if m:
|
|
442
|
+
return m.group(1) if m.group(1) is not None else m.group(2)
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _after_word(text: str, words: tuple) -> Optional[str]:
|
|
447
|
+
for w in words:
|
|
448
|
+
m = re.search(r"\b" + w + r"\s+(.+)$", text, re.IGNORECASE)
|
|
449
|
+
if m:
|
|
450
|
+
return m.group(1).strip()
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def map_step_to_ir(step: GherkinStep) -> StepIR:
|
|
455
|
+
"""Maps a Gherkin step onto a StepIR action using deterministic rules.
|
|
456
|
+
|
|
457
|
+
Best-effort by design: unrecognizable phrasing falls back to CUSTOM_CALL
|
|
458
|
+
with the original text preserved in `description`.
|
|
459
|
+
"""
|
|
460
|
+
text = step.text
|
|
461
|
+
lower = text.lower()
|
|
462
|
+
quoted = _first_quoted(text)
|
|
463
|
+
role = step.role
|
|
464
|
+
|
|
465
|
+
if role == "assert":
|
|
466
|
+
if re.search(r"\burl\b", lower) and re.search(r"\b(should|must|is|contains)\b", lower):
|
|
467
|
+
expected = quoted or (URL_PATTERN.search(text).group(0) if URL_PATTERN.search(text) else text)
|
|
468
|
+
return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.URL_CONTAINS,
|
|
469
|
+
expected=expected, description=text)
|
|
470
|
+
# A quoted expectation ("should see "Order Confirmed"") is a text
|
|
471
|
+
# assertion - stronger than a bare visibility check, so it wins.
|
|
472
|
+
if quoted and re.search(r"\b(see|sees|contains|shows|displays|message|text)\b", lower):
|
|
473
|
+
return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.CONTAINS_TEXT,
|
|
474
|
+
target=None, expected=quoted, description=text)
|
|
475
|
+
if re.search(r"\b(see|sees|visible|displayed|shown|appears|exists)\b", lower):
|
|
476
|
+
return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.VISIBLE,
|
|
477
|
+
target=quoted, expected=quoted, description=text)
|
|
478
|
+
if quoted:
|
|
479
|
+
return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.CONTAINS_TEXT,
|
|
480
|
+
expected=quoted, description=text)
|
|
481
|
+
return StepIR(action=ActionType.ASSERT, assertion_type=AssertionType.VISIBLE,
|
|
482
|
+
description=text)
|
|
483
|
+
|
|
484
|
+
if role == "act":
|
|
485
|
+
# Verbs are matched anywhere in the sentence: Gherkin steps commonly
|
|
486
|
+
# start with a subject ("the user clicks ..."), including 3rd person.
|
|
487
|
+
if re.search(r"\b(navigates?|go(?:es)?\s+to|opens?|visits?|launch(?:es)?|browse(?:s)?)\b", lower):
|
|
488
|
+
url_match = URL_PATTERN.search(text)
|
|
489
|
+
return StepIR(action=ActionType.GOTO, value=url_match.group(0) if url_match else quoted,
|
|
490
|
+
description=text)
|
|
491
|
+
if re.search(r"\b(fill(?:s)?\s+in|enter(?:s)?|type(?:s)?|input(?:s)?|provide(?:s)?|set(?:s)?)\b", lower):
|
|
492
|
+
quotes = [
|
|
493
|
+
m.group(1) if m.group(1) is not None else m.group(2)
|
|
494
|
+
for m in QUOTE_PATTERN.finditer(text)
|
|
495
|
+
]
|
|
496
|
+
value = quotes[0] if quotes else None
|
|
497
|
+
if len(quotes) >= 2:
|
|
498
|
+
target = quotes[1]
|
|
499
|
+
else:
|
|
500
|
+
target = _after_word(text, ("into", "in the", "in", "on"))
|
|
501
|
+
return StepIR(action=ActionType.FILL, target=target, value=value, description=text)
|
|
502
|
+
if re.search(r"\b(select|choose|pick)", lower):
|
|
503
|
+
target = _after_word(text, ("from", "in"))
|
|
504
|
+
return StepIR(action=ActionType.SELECT, target=target, value=quoted, description=text)
|
|
505
|
+
if re.search(r"\b(uncheck|untick|deselect|clear)", lower):
|
|
506
|
+
return StepIR(action=ActionType.UNCHECK, target=quoted, description=text)
|
|
507
|
+
if re.search(r"\b(check|tick)", lower):
|
|
508
|
+
return StepIR(action=ActionType.CHECK, target=quoted, description=text)
|
|
509
|
+
if re.search(r"\b(hover|mouse\s?over)", lower):
|
|
510
|
+
return StepIR(action=ActionType.HOVER, target=quoted, description=text)
|
|
511
|
+
if re.search(r"\bwait", lower):
|
|
512
|
+
return StepIR(action=ActionType.WAIT, wait_strategy=WaitStrategy.AUTO, description=text)
|
|
513
|
+
if re.search(r"\b(double.?click|dblclick)", lower):
|
|
514
|
+
return StepIR(action=ActionType.DBLCLICK, target=quoted, description=text)
|
|
515
|
+
if re.search(r"\b(click|tap|press|submit)", lower):
|
|
516
|
+
target = quoted or _after_word(text, ("on the", "on", "the"))
|
|
517
|
+
return StepIR(action=ActionType.CLICK, target=target, description=text)
|
|
518
|
+
|
|
519
|
+
# arrange (Given) and any unmapped phrasing -> explicit custom call
|
|
520
|
+
return StepIR(action=ActionType.CUSTOM_CALL, description=f"{step.keyword} {text}")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def build_test_case_ir(case: Dict[str, Any]) -> TestCaseIR:
|
|
524
|
+
steps = [map_step_to_ir(s) for s in case["steps"]]
|
|
525
|
+
return TestCaseIR(
|
|
526
|
+
id=case["id"],
|
|
527
|
+
title=case["name"],
|
|
528
|
+
steps=steps,
|
|
529
|
+
tags=case["tags"],
|
|
530
|
+
description=case.get("description", ""),
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# ---------------------------------------------------------------------------
|
|
535
|
+
# "Proven vs owed" coverage-gap analysis (idea re-expressed from Kane CLI)
|
|
536
|
+
# ---------------------------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
def analyze_gaps(feature: GherkinFeature, cases: List[Dict[str, Any]]) -> List[Dict[str, str]]:
|
|
539
|
+
gaps: List[Dict[str, str]] = []
|
|
540
|
+
|
|
541
|
+
for case in cases:
|
|
542
|
+
if not any(s.role == "assert" for s in case["steps"]):
|
|
543
|
+
gaps.append({
|
|
544
|
+
"severity": "WARNING",
|
|
545
|
+
"rule": "G1-MISSING_ASSERTION",
|
|
546
|
+
"target": case["id"],
|
|
547
|
+
"message": "Case has no Then/assert step - add an observable outcome assertion.",
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
has_negative = any(
|
|
551
|
+
NEGATIVE_SIGNAL_PATTERN.search(" ".join([sc.name] + sc.tags)) for sc in feature.scenarios
|
|
552
|
+
)
|
|
553
|
+
if not has_negative:
|
|
554
|
+
gaps.append({
|
|
555
|
+
"severity": "WARNING",
|
|
556
|
+
"rule": "G2-MISSING_NEGATIVE_PATH",
|
|
557
|
+
"target": feature.name or feature.file,
|
|
558
|
+
"message": "No negative/invalid scenario detected - add at least one "
|
|
559
|
+
"(e.g. invalid input, unauthorized access, empty state).",
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
for scenario in feature.scenarios:
|
|
563
|
+
if not scenario.is_outline:
|
|
564
|
+
continue
|
|
565
|
+
for block in scenario.examples:
|
|
566
|
+
if block.header is not None and len(block.rows) < 2:
|
|
567
|
+
gaps.append({
|
|
568
|
+
"severity": "WARNING",
|
|
569
|
+
"rule": "G3-THIN_EXAMPLES",
|
|
570
|
+
"target": scenario.name,
|
|
571
|
+
"message": f"Examples table has {len(block.rows)} row(s) - add boundary and "
|
|
572
|
+
"negative rows (empty, max length, special chars).",
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
return gaps
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# ---------------------------------------------------------------------------
|
|
579
|
+
# Emitters
|
|
580
|
+
# ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
def emit_markdown_plan(feature: GherkinFeature, cases: List[Dict[str, Any]],
|
|
583
|
+
gaps: List[Dict[str, str]]) -> str:
|
|
584
|
+
lines: List[str] = []
|
|
585
|
+
lines.append(f"# BDD Test Plan - {feature.name or os.path.basename(feature.file)}")
|
|
586
|
+
lines.append("")
|
|
587
|
+
if feature.tags:
|
|
588
|
+
lines.append("**Tags:** " + " ".join(feature.tags))
|
|
589
|
+
if feature.description:
|
|
590
|
+
lines.append(f"> {feature.description}")
|
|
591
|
+
lines.append(f"- Source: `{feature.file}`")
|
|
592
|
+
lines.append(f"- Scenarios: {len(feature.scenarios)} | Expanded cases: {len(cases)}")
|
|
593
|
+
lines.append("")
|
|
594
|
+
|
|
595
|
+
if feature.background:
|
|
596
|
+
lines.append("## Background")
|
|
597
|
+
for step in feature.background:
|
|
598
|
+
lines.append(f"- **{step.keyword}** {step.display_text()}")
|
|
599
|
+
lines.append("")
|
|
600
|
+
|
|
601
|
+
for scenario in feature.scenarios:
|
|
602
|
+
kind = "Scenario Outline" if scenario.is_outline else "Scenario"
|
|
603
|
+
heading = f"## {kind}: {scenario.name}"
|
|
604
|
+
if scenario.rule_name:
|
|
605
|
+
heading += f" (Rule: {scenario.rule_name})"
|
|
606
|
+
lines.append(heading)
|
|
607
|
+
if scenario.tags:
|
|
608
|
+
lines.append(" ".join(scenario.tags))
|
|
609
|
+
lines.append("")
|
|
610
|
+
lines.append("| # | Step | Role | Mapped Action |")
|
|
611
|
+
lines.append("| :--- | :--- | :--- | :--- |")
|
|
612
|
+
case_for_scenario = next((c for c in cases if c["scenario"] == scenario.name), None)
|
|
613
|
+
step_source = case_for_scenario["steps"] if case_for_scenario else scenario.steps
|
|
614
|
+
for idx, step in enumerate(step_source, 1):
|
|
615
|
+
ir = map_step_to_ir(step)
|
|
616
|
+
lines.append(f"| {idx} | **{step.keyword}** {step.text.splitlines()[0]} | {step.role} | `{ir.action.value}` |")
|
|
617
|
+
if scenario.is_outline:
|
|
618
|
+
for block in scenario.examples:
|
|
619
|
+
lines.append("")
|
|
620
|
+
label = f"Examples: {block.name}" if block.name else "Examples"
|
|
621
|
+
tags = " " + " ".join(block.tags) if block.tags else ""
|
|
622
|
+
lines.append(f"**{label}**{tags}")
|
|
623
|
+
if block.header:
|
|
624
|
+
lines.append("")
|
|
625
|
+
lines.append("| " + " | ".join(block.header) + " |")
|
|
626
|
+
lines.append("|" + "---|" * len(block.header))
|
|
627
|
+
for row in block.rows:
|
|
628
|
+
lines.append("| " + " | ".join(row[h] for h in block.header) + " |")
|
|
629
|
+
lines.append("")
|
|
630
|
+
|
|
631
|
+
lines.append("## Coverage Gaps (proven vs owed)")
|
|
632
|
+
if gaps:
|
|
633
|
+
for gap in gaps:
|
|
634
|
+
lines.append(f"- **[{gap['severity']}] {gap['rule']}** `{gap['target']}` - {gap['message']}")
|
|
635
|
+
else:
|
|
636
|
+
lines.append("- No gaps detected.")
|
|
637
|
+
lines.append("")
|
|
638
|
+
return "\n".join(lines)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def compile_feature(path: str) -> Dict[str, Any]:
|
|
642
|
+
"""Parses, expands, maps and audits one .feature file into a result dict."""
|
|
643
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
644
|
+
text = f.read()
|
|
645
|
+
|
|
646
|
+
parser = GherkinParser()
|
|
647
|
+
feature = parser.parse(text, filename=os.path.relpath(path, REPO_ROOT)
|
|
648
|
+
if path.startswith(REPO_ROOT) else path)
|
|
649
|
+
errors = list(parser.errors)
|
|
650
|
+
|
|
651
|
+
cases = expand_feature(feature, errors)
|
|
652
|
+
error_count = sum(len(c["errors"]) for c in cases) + len(errors)
|
|
653
|
+
gaps = analyze_gaps(feature, cases)
|
|
654
|
+
|
|
655
|
+
mapped = {c["id"]: [map_step_to_ir(s).to_dict() for s in c["steps"]] for c in cases}
|
|
656
|
+
total = len(cases)
|
|
657
|
+
invalid = sum(1 for c in cases if c["errors"])
|
|
658
|
+
health_score = 100.0 if total == 0 else max(0.0, round(((total - invalid) / total) * 100, 1))
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
"timestamp": datetime.now().isoformat(),
|
|
662
|
+
"file": feature.file,
|
|
663
|
+
"feature": feature.name,
|
|
664
|
+
"tags": feature.tags,
|
|
665
|
+
"totals": {
|
|
666
|
+
"scenarios": len(feature.scenarios),
|
|
667
|
+
"expanded_cases": total,
|
|
668
|
+
"steps": sum(len(c["steps"]) for c in cases),
|
|
669
|
+
"invalid_cases": invalid,
|
|
670
|
+
},
|
|
671
|
+
"errors": errors + [f"{c['id']}: {e}" for c in cases for e in c["errors"]],
|
|
672
|
+
"gaps": gaps,
|
|
673
|
+
"cases": [
|
|
674
|
+
{
|
|
675
|
+
"id": c["id"],
|
|
676
|
+
"name": c["name"],
|
|
677
|
+
"tags": c["tags"],
|
|
678
|
+
"steps": mapped[c["id"]],
|
|
679
|
+
"valid": not c["errors"],
|
|
680
|
+
}
|
|
681
|
+
for c in cases
|
|
682
|
+
],
|
|
683
|
+
"health_score_pct": health_score,
|
|
684
|
+
"markdown_plan": emit_markdown_plan(feature, cases, gaps),
|
|
685
|
+
"_feature": feature,
|
|
686
|
+
"_ir_cases": [
|
|
687
|
+
{"id": c["id"], "ir": build_test_case_ir(c).to_dict()}
|
|
688
|
+
for c in cases if not c["errors"]
|
|
689
|
+
],
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def run(target: str, as_json: bool = False, emit_ir: bool = False) -> int:
|
|
694
|
+
if os.path.isfile(target):
|
|
695
|
+
feature_files = [target]
|
|
696
|
+
elif os.path.isdir(target):
|
|
697
|
+
feature_files = sorted(glob.glob(os.path.join(target, "**", "*.feature"), recursive=True))
|
|
698
|
+
else:
|
|
699
|
+
print(f"{RED}[ERROR]{RESET} Target not found: {target}")
|
|
700
|
+
return 1
|
|
701
|
+
if not feature_files:
|
|
702
|
+
print(f"{YELLOW}[WARN]{RESET} No .feature files discovered under: {target}")
|
|
703
|
+
return 1
|
|
704
|
+
|
|
705
|
+
results = [compile_feature(p) for p in feature_files]
|
|
706
|
+
all_errors = [e for r in results for e in r["errors"]]
|
|
707
|
+
total_cases = sum(r["totals"]["expanded_cases"] for r in results)
|
|
708
|
+
total_gaps = sum(len(r["gaps"]) for r in results)
|
|
709
|
+
|
|
710
|
+
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
711
|
+
print(f"{BOLD}{CYAN} GHERKIN BDD SUITE COMPILER (Natural-Language Test Ingestion) {RESET}")
|
|
712
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
713
|
+
print(f" Feature Files : {len(feature_files)}")
|
|
714
|
+
print(f" Expanded Test Cases : {GREEN if total_cases else RED}{total_cases}{RESET}")
|
|
715
|
+
print(f" Coverage Gap Warnings : {YELLOW if total_gaps else GREEN}{total_gaps}{RESET}")
|
|
716
|
+
print(f" Errors : {RED if all_errors else GREEN}{len(all_errors)}{RESET}")
|
|
717
|
+
scores = [r["health_score_pct"] for r in results if r["totals"]["expanded_cases"]]
|
|
718
|
+
avg = round(sum(scores) / len(scores), 1) if scores else 0.0
|
|
719
|
+
print(f" Case Validity Score : {GREEN if avg == 100 else YELLOW}{avg}%{RESET}")
|
|
720
|
+
print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
|
|
721
|
+
|
|
722
|
+
for r in results:
|
|
723
|
+
for e in r["errors"]:
|
|
724
|
+
print(f" - [{RED}ERROR{RESET}] {e}")
|
|
725
|
+
for g in r["gaps"]:
|
|
726
|
+
print(f" - [{YELLOW}{g['severity']}{RESET}] {g['rule']} {g['target']} - {g['message']}")
|
|
727
|
+
|
|
728
|
+
os.makedirs(REPORTS_DIR, exist_ok=True)
|
|
729
|
+
persist = [{k: v for k, v in r.items() if not k.startswith("_")} for r in results]
|
|
730
|
+
with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
|
|
731
|
+
json.dump({"timestamp": datetime.now().isoformat(), "features": persist}, f,
|
|
732
|
+
indent=2, ensure_ascii=False)
|
|
733
|
+
print(f"Detailed audit saved to: {OUTPUT_JSON}\n")
|
|
734
|
+
|
|
735
|
+
if emit_ir:
|
|
736
|
+
ir_payload = [case["ir"] for r in results for case in r["_ir_cases"]]
|
|
737
|
+
print(json.dumps(ir_payload, indent=2, ensure_ascii=False))
|
|
738
|
+
|
|
739
|
+
if as_json:
|
|
740
|
+
print(json.dumps(persist, indent=2, ensure_ascii=False))
|
|
741
|
+
|
|
742
|
+
return 1 if all_errors else 0
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def main() -> int:
|
|
746
|
+
parser = argparse.ArgumentParser(
|
|
747
|
+
description="Gherkin BDD Suite Compiler - compiles .feature files into a test matrix, "
|
|
748
|
+
"Universal IR and coverage-gap report (0 LLM tokens).")
|
|
749
|
+
parser.add_argument("target", help="A .feature file or a directory containing .feature files")
|
|
750
|
+
parser.add_argument("--json", action="store_true", help="Print the full JSON report to stdout")
|
|
751
|
+
parser.add_argument("--emit-ir", action="store_true",
|
|
752
|
+
help="Print hub Universal IR (TestCaseIR) dicts for the omni pipeline")
|
|
753
|
+
args = parser.parse_args()
|
|
754
|
+
return run(args.target, as_json=args.json, emit_ir=args.emit_ir)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
if __name__ == "__main__":
|
|
758
|
+
sys.exit(main())
|