@sun-asterisk/sungen 3.2.24 → 3.2.26
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/dist/capabilities/context-router.d.ts.map +1 -1
- package/dist/capabilities/context-router.js +15 -7
- package/dist/capabilities/context-router.js.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +23 -4
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +6 -48
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/changed.d.ts +14 -0
- package/dist/cli/commands/changed.d.ts.map +1 -0
- package/dist/cli/commands/changed.js +128 -0
- package/dist/cli/commands/changed.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +25 -1
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.js +1 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts +19 -0
- package/dist/cli/commands/results-merge.d.ts.map +1 -0
- package/dist/cli/commands/results-merge.js +191 -0
- package/dist/cli/commands/results-merge.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +100 -12
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
- package/dist/exporters/api-catalog-loader.js +10 -3
- package/dist/exporters/api-catalog-loader.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +9 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +55 -3
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +10 -1
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +6 -1
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +14 -1
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.js +1 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -1
- package/dist/exporters/matrix/map-loader.js +5 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +5 -1
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +4 -1
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +18 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +17 -2
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +68 -8
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/results-merge.d.ts +46 -0
- package/dist/exporters/results-merge.d.ts.map +1 -0
- package/dist/exporters/results-merge.js +148 -0
- package/dist/exporters/results-merge.js.map +1 -0
- package/dist/exporters/selector-key-resolver.d.ts +4 -0
- package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
- package/dist/exporters/selector-key-resolver.js +101 -1
- package/dist/exporters/selector-key-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +8 -1
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +5 -4
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +2 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +52 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/dist/generators/test-generator/code-generator.d.ts +6 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +80 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +60 -6
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +59 -5
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +19 -12
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +58 -38
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +23 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
- package/dist/generators/test-generator/recipe-expander.js +9 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- package/dist/generators/test-generator/reset-tier.d.ts +68 -0
- package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
- package/dist/generators/test-generator/reset-tier.js +94 -0
- package/dist/generators/test-generator/reset-tier.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +39 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +144 -5
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +2 -10
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +16 -2
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
- package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/format-pattern.js +82 -0
- package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +20 -1
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
- package/dist/harness/annotation-overrides.d.ts +0 -12
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +9 -5
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/audit.d.ts +7 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +78 -10
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +8 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/capability.d.ts +11 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +8 -0
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +5 -0
- package/dist/harness/flow-contract.d.ts +18 -1
- package/dist/harness/flow-contract.d.ts.map +1 -1
- package/dist/harness/flow-contract.js +72 -9
- package/dist/harness/flow-contract.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +5 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +12 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +62 -7
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/run-manifest.d.ts +58 -0
- package/dist/harness/run-manifest.d.ts.map +1 -0
- package/dist/harness/run-manifest.js +264 -0
- package/dist/harness/run-manifest.js.map +1 -0
- package/dist/harness/spec-branches.d.ts +88 -0
- package/dist/harness/spec-branches.d.ts.map +1 -0
- package/dist/harness/spec-branches.js +280 -0
- package/dist/harness/spec-branches.js.map +1 -0
- package/dist/harness/spec-coverage.d.ts +1 -1
- package/dist/harness/spec-coverage.js +4 -4
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/viewpoint-baseline.d.ts +9 -0
- package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
- package/dist/harness/viewpoint-baseline.js +33 -3
- package/dist/harness/viewpoint-baseline.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +88 -9
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/lockstep-version.d.ts +22 -0
- package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
- package/dist/orchestrator/lockstep-version.js +58 -0
- package/dist/orchestrator/lockstep-version.js.map +1 -0
- package/dist/orchestrator/project-deps-sync.d.ts +39 -0
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
- package/dist/orchestrator/project-deps-sync.js +175 -0
- package/dist/orchestrator/project-deps-sync.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +5 -2
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +9 -2
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/dist/orchestrator/templates/env.appium.example +10 -0
- package/dist/orchestrator/templates/specs-api.d.ts +4 -2
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +233 -10
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +214 -11
- package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
- package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
- package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/dist/orchestrator/templates/specs-test-data.ts +53 -6
- package/dist/orchestrator/templates/wdio.conf.ts +28 -9
- package/package.json +8 -6
- package/src/capabilities/context-router.ts +13 -5
- package/src/cli/commands/audit.ts +22 -3
- package/src/cli/commands/capability.ts +4 -38
- package/src/cli/commands/changed.ts +104 -0
- package/src/cli/commands/delivery.ts +27 -2
- package/src/cli/commands/generate.ts +1 -1
- package/src/cli/commands/results-merge.ts +153 -0
- package/src/cli/commands/update.ts +72 -13
- package/src/cli/index.ts +4 -0
- package/src/exporters/api-catalog-loader.ts +10 -2
- package/src/exporters/api-testcase-formatter.ts +59 -3
- package/src/exporters/csv-exporter.ts +11 -2
- package/src/exporters/json-exporter.ts +7 -2
- package/src/exporters/matrix/build.ts +13 -1
- package/src/exporters/matrix/export.ts +1 -1
- package/src/exporters/matrix/map-loader.ts +5 -0
- package/src/exporters/matrix/render-csv.ts +5 -1
- package/src/exporters/matrix/render-xlsx.ts +4 -1
- package/src/exporters/matrix/types.ts +18 -0
- package/src/exporters/playwright-report-parser.ts +68 -9
- package/src/exporters/results-merge.ts +150 -0
- package/src/exporters/selector-key-resolver.ts +81 -1
- package/src/exporters/types.ts +8 -1
- package/src/exporters/xlsx-report-builder.ts +5 -4
- package/src/generators/gherkin-parser/index.ts +55 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/src/generators/test-generator/code-generator.ts +86 -7
- package/src/generators/test-generator/diagnostics.ts +60 -6
- package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
- package/src/generators/test-generator/patterns/index.ts +58 -33
- package/src/generators/test-generator/patterns/types.ts +36 -0
- package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
- package/src/generators/test-generator/recipe-expander.ts +9 -1
- package/src/generators/test-generator/reset-tier.ts +93 -0
- package/src/generators/test-generator/step-mapper.ts +149 -6
- package/src/generators/test-generator/template-engine.ts +18 -3
- package/src/generators/test-generator/utils/format-pattern.ts +79 -0
- package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
- package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
- package/src/generators/test-generator/utils/selector-resolver.ts +22 -1
- package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
- package/src/harness/annotation-overrides.ts +10 -5
- package/src/harness/audit.ts +82 -12
- package/src/harness/capability-plan.ts +10 -5
- package/src/harness/capability.ts +16 -0
- package/src/harness/catalog/drivers.yaml +5 -0
- package/src/harness/flow-contract.ts +87 -9
- package/src/harness/parse.ts +5 -1
- package/src/harness/quality-gates.ts +64 -6
- package/src/harness/run-manifest.ts +254 -0
- package/src/harness/spec-branches.ts +346 -0
- package/src/harness/spec-coverage.ts +4 -4
- package/src/harness/viewpoint-baseline.ts +41 -6
- package/src/harness/viewpoint-ledger.ts +82 -9
- package/src/index.ts +1 -0
- package/src/orchestrator/lockstep-version.ts +46 -0
- package/src/orchestrator/project-deps-sync.ts +175 -0
- package/src/orchestrator/project-initializer.ts +5 -2
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +16 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +9 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +40 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/src/orchestrator/templates/env.appium.example +10 -0
- package/src/orchestrator/templates/specs-api.ts +214 -11
- package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/src/orchestrator/templates/specs-test-data.ts +53 -6
- package/src/orchestrator/templates/wdio.conf.ts +28 -9
|
@@ -98,7 +98,7 @@ function callBlockLines(call: ApiCall, entry: ApiCatalogEntry, localize: (text:
|
|
|
98
98
|
const filesStr = formatFiles(entry.files, call.args, localize);
|
|
99
99
|
const bodyFileStr = formatBodyFile(entry.bodyFile, call.args, localize);
|
|
100
100
|
// Body is scalar fields; a pure file upload may have none → still show `Body: —` for a stable shape.
|
|
101
|
-
lines.push(`Body: ${formatBody(entry.body, call.args, localize)}`);
|
|
101
|
+
lines.push(`Body: ${formatBody(entry.body, call.args, localize, entry)}`);
|
|
102
102
|
if (filesStr) lines.push(`Files: ${filesStr}`);
|
|
103
103
|
if (bodyFileStr) lines.push(`Body file: ${bodyFileStr}`);
|
|
104
104
|
return lines;
|
|
@@ -111,6 +111,24 @@ function callBlockLines(call: ApiCall, entry: ApiCatalogEntry, localize: (text:
|
|
|
111
111
|
* has more than one call — same style as formatApiRequest — which also keeps a
|
|
112
112
|
* deliberately repeated call distinct through the matrix's precondition Set-dedup.
|
|
113
113
|
*/
|
|
114
|
+
/**
|
|
115
|
+
* Terse one-line-per-call summary for a screen/flow row's PRE-CONDITION cell:
|
|
116
|
+
* `Call API <name> — <METHOD> <path>`. Built on the strict parseApiCalls so the
|
|
117
|
+
* deliverable never documents a call the runtime silently dropped (a malformed
|
|
118
|
+
* tag never runs). Only name/method/path from the catalog render — tag args
|
|
119
|
+
* carry runtime values. A missing endpoint is loud; a present entry missing a
|
|
120
|
+
* field shows `—` for that field instead of blaming the wrong file.
|
|
121
|
+
*/
|
|
122
|
+
export function formatApiCallSummaryLines(tags: string[], catalog: Record<string, ApiCatalogEntry>): string[] {
|
|
123
|
+
return parseApiCalls(tags).map(({ name }) => {
|
|
124
|
+
const entry = catalog[name];
|
|
125
|
+
if (!entry) return `Call API ${name} — (endpoint not in apis.yaml)`;
|
|
126
|
+
const method = typeof entry.method === 'string' && entry.method ? entry.method.toUpperCase() : '—';
|
|
127
|
+
const apiPath = typeof entry.path === 'string' && entry.path ? entry.path : '—';
|
|
128
|
+
return `Call API ${name} — ${method} ${apiPath}`;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
114
132
|
export function formatApiCallLines(
|
|
115
133
|
calls: ApiCall[],
|
|
116
134
|
catalog: Record<string, ApiCatalogEntry>,
|
|
@@ -185,6 +203,38 @@ function bindParams(raw: string, args: Record<string, string>): string {
|
|
|
185
203
|
});
|
|
186
204
|
}
|
|
187
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Whitelist variant for XML string bodies: bind ONLY the declared param names
|
|
208
|
+
* (longest first, word-boundary guarded) — the generic `:word` scan above would
|
|
209
|
+
* rewrite namespace prefixes (soapenv:Envelope) into garbage cells. Mirrors the
|
|
210
|
+
* runtime's whitelist binding.
|
|
211
|
+
*/
|
|
212
|
+
function bindXmlParamsForCell(raw: string, args: Record<string, string>, paramNames: string[]): string {
|
|
213
|
+
let out = raw;
|
|
214
|
+
for (const p of [...paramNames].sort((a, b) => b.length - a.length)) {
|
|
215
|
+
const expr = p in args ? args[p] : `{{${p}}}`;
|
|
216
|
+
out = out.replace(new RegExp(`:${p}(?![A-Za-z0-9_])`, 'g'), () => unquoteExpr(expr));
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Mask the value of credential-named ELEMENTS inside an XML string body
|
|
223
|
+
* (`<signature>…</signature>` → `<signature>***</signature>`). The object-body
|
|
224
|
+
* masking below never sees a string template, so without this the raw signature
|
|
225
|
+
* from the catalog example would print verbatim in the client's API detail sheet.
|
|
226
|
+
*/
|
|
227
|
+
function maskXmlSensitiveElements(xml: string): string {
|
|
228
|
+
// The open tag may carry attributes (<signature type="hmac">) and the value may
|
|
229
|
+
// be a CDATA section — both are common spellings for exactly the credential
|
|
230
|
+
// elements this masks, so the pattern must accept them or the secret prints raw.
|
|
231
|
+
return xml.replace(
|
|
232
|
+
/(<([A-Za-z0-9_.:-]+)(?:\s[^>]*)?>)((?:<!\[CDATA\[[\s\S]*?\]\]>|[^<])*)(<\/\2>)/g,
|
|
233
|
+
(whole, open: string, tag: string, _value: string, close: string) =>
|
|
234
|
+
SENSITIVE_NAME.test(tag) ? `${open}***${close}` : whole,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
188
238
|
/** Strip a matching pair of surrounding quotes from a tag-arg literal (`"x"` → `x`). */
|
|
189
239
|
function unquoteExpr(e: string): string {
|
|
190
240
|
const m = e.match(/^"(.*)"$/) || e.match(/^'(.*)'$/);
|
|
@@ -230,6 +280,7 @@ function formatHeaders(
|
|
|
230
280
|
function contentTypeFor(encoding?: string): string {
|
|
231
281
|
if (encoding === 'form') return 'application/x-www-form-urlencoded';
|
|
232
282
|
if (encoding === 'multipart') return 'multipart/form-data';
|
|
283
|
+
if (encoding === 'xml') return 'application/xml';
|
|
233
284
|
return 'application/json';
|
|
234
285
|
}
|
|
235
286
|
|
|
@@ -247,8 +298,13 @@ function uploadContentType(entry: ApiCatalogEntry): string {
|
|
|
247
298
|
* that field can be replaced with `***`; otherwise the body resolves wholesale
|
|
248
299
|
* (preserving numbers/nesting exactly as before).
|
|
249
300
|
*/
|
|
250
|
-
function formatBody(body: unknown, args: Record<string, string>, localize: (text: string) => string): string {
|
|
301
|
+
function formatBody(body: unknown, args: Record<string, string>, localize: (text: string) => string, entry?: ApiCatalogEntry): string {
|
|
251
302
|
if (body === undefined || body === null) return '—';
|
|
303
|
+
// XML string template: whitelist-bind declared params, then mask credential elements.
|
|
304
|
+
if (entry?.encoding === 'xml' && typeof body === 'string') {
|
|
305
|
+
const names = Array.isArray(entry.params) ? (entry.params as string[]) : [];
|
|
306
|
+
return localize(maskXmlSensitiveElements(bindXmlParamsForCell(body, args, names)));
|
|
307
|
+
}
|
|
252
308
|
if (isPlainObject(body)) {
|
|
253
309
|
const obj = body as Record<string, unknown>;
|
|
254
310
|
if (Object.keys(obj).some(isSensitiveField)) {
|
|
@@ -321,7 +377,7 @@ function formatBodyFile(bodyFile: unknown, args: Record<string, string>, localiz
|
|
|
321
377
|
* test-data keys — catches variants like confirmPassword / new_password /
|
|
322
378
|
* password_confirmation / access_token / client_secret / otp / passcode.
|
|
323
379
|
*/
|
|
324
|
-
export const SENSITIVE_NAME = /pass(word|code)?|passwd|pwd|secret|token|otp|api[-_]?key|apikey|credential|authorization/i;
|
|
380
|
+
export const SENSITIVE_NAME = /pass(word|code)?|passwd|pwd|secret|token|otp|api[-_]?key|apikey|credential|authorization|signature/i;
|
|
325
381
|
function isSensitiveField(key: string): boolean {
|
|
326
382
|
return SENSITIVE_NAME.test(key);
|
|
327
383
|
}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
statusToTestResult,
|
|
24
24
|
} from './playwright-report-parser';
|
|
25
25
|
import { ApiCatalogEntry, EnvironmentInfo, PlaywrightResult, ScreenSummary, TestCaseRow } from './types';
|
|
26
|
-
import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
|
|
26
|
+
import { collectSecretValues, formatApiCallSummaryLines, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
|
|
27
27
|
import { SelectorKeyMap, substituteSelectorKeysInStep } from './selector-key-resolver';
|
|
28
28
|
import { formatVariantRow, resolveResultVariants } from './result-variants';
|
|
29
29
|
import { collectQaEnvSecretDenylist, warnIfRedactionIncomplete } from './qa-env-secret-denylist';
|
|
@@ -103,9 +103,18 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
103
103
|
};
|
|
104
104
|
const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
105
105
|
localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
106
|
+
// `@api:` preconditions on a screen/flow scenario run before its UI steps —
|
|
107
|
+
// without these lines the deliverable never names the real endpoint (QA
|
|
108
|
+
// post-processed every export to add them back). They belong in the
|
|
109
|
+
// PRE-CONDITION cell, the same home Background moved to: inlining setup in
|
|
110
|
+
// Steps shifts the numbering and every `— after step N` anchor with it.
|
|
111
|
+
// kind:api keeps its own richer request-block rendering below.
|
|
112
|
+
const apiCallLines = (input.kind === 'screen' || input.kind === 'flow') && input.apiCatalog && hasApiCalls(m.feature.tags)
|
|
113
|
+
? formatApiCallSummaryLines(m.feature.tags, input.apiCatalog)
|
|
114
|
+
: [];
|
|
106
115
|
const precondition = formatPrecondition(
|
|
107
116
|
authRole,
|
|
108
|
-
(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean)
|
|
117
|
+
[...(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean), ...apiCallLines]
|
|
109
118
|
);
|
|
110
119
|
const stepLines = m.resolvedSteps.map(formatStep).filter(Boolean);
|
|
111
120
|
// Scenarios whose assertions verify different moments render Expected as
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from './feature-parser';
|
|
17
17
|
import { formatPrecondition, groupExpectedByAnchor, keepActor, stripActor } from './step-formatter';
|
|
18
18
|
import { formatTestData, substituteTestDataVars } from './test-data-resolver';
|
|
19
|
-
import { collectSecretValues, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
|
|
19
|
+
import { collectSecretValues, formatApiCallSummaryLines, formatApiExpected, formatApiRequest, hasApiCalls, redactSecrets } from './api-testcase-formatter';
|
|
20
20
|
import {
|
|
21
21
|
formatExecutedDate,
|
|
22
22
|
statusToTestResult,
|
|
@@ -99,9 +99,14 @@ export function buildScreenSnapshot(input: BuildScreenSnapshotInput): ScreenSnap
|
|
|
99
99
|
};
|
|
100
100
|
const formatStep = (s: { text: string; bucket: 'given' | 'when' | 'then' }) =>
|
|
101
101
|
localize(s.bucket === 'given' ? keepActor(s.text) : stripActor(s.text));
|
|
102
|
+
// Same `@api:` precondition lines as csv-exporter's buildTestCaseRows — the
|
|
103
|
+
// dashboard and the delivery CSV must not disagree about what a row does.
|
|
104
|
+
const apiCallLines = (input.kind === 'screen' || input.kind === 'flow') && input.apiCatalog && hasApiCalls(m.feature.tags)
|
|
105
|
+
? formatApiCallSummaryLines(m.feature.tags, input.apiCatalog)
|
|
106
|
+
: [];
|
|
102
107
|
const precondition = formatPrecondition(
|
|
103
108
|
authRole,
|
|
104
|
-
(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean)
|
|
109
|
+
[...(m.resolvedBackgroundSteps ?? []).map(formatStep).filter(Boolean), ...apiCallLines]
|
|
105
110
|
);
|
|
106
111
|
let steps = m.resolvedSteps.map(formatStep).filter(Boolean);
|
|
107
112
|
// Same anchor rule as csv-exporter (shared groupExpectedByAnchor) so the
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { ApiCatalogEntry, FeatureMetadata, PlaywrightResult } from '../types';
|
|
9
9
|
import { formatApiCallLines, parseApiCalls } from '../api-testcase-formatter';
|
|
10
|
+
import { collectQaEnvSecretDenylist } from '../qa-env-secret-denylist';
|
|
10
11
|
import { bucketTexts } from './step-buckets';
|
|
11
12
|
import { MergedScenario } from '../scenario-merger';
|
|
12
13
|
import {
|
|
@@ -140,6 +141,10 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
140
141
|
const { vpId, category1 } = splitVpAndName(m.feature.name);
|
|
141
142
|
if (!vpId) continue; // Gate A reports it — no variant can be addressed without a VP-id
|
|
142
143
|
const tags = m.feature.tags;
|
|
144
|
+
// @exploration probes live outside the official suite (the same contract the audit's
|
|
145
|
+
// source-tracing exemption follows) — they never become deliverable coverage variants,
|
|
146
|
+
// so the gates and the map never ask for them.
|
|
147
|
+
if (tags.some((t) => /^@exploration\b/i.test(t))) continue;
|
|
143
148
|
const mode = extractTestcaseType(tags) === 'Manual' ? 'manual' : 'auto';
|
|
144
149
|
const authRole = extractAuthRole(tags);
|
|
145
150
|
// Category segment of the id, whatever the project's scheme:
|
|
@@ -513,6 +518,9 @@ export function expandMapRef(ref: string, variantsByVp: Map<string, CoverageVari
|
|
|
513
518
|
export function buildMatrix(inputs: BuildInputs): MatrixModel {
|
|
514
519
|
const { unit, map, transformerVersion } = inputs;
|
|
515
520
|
const variants = deriveVariants(inputs);
|
|
521
|
+
// Same denylist the testcases CSV applies to its Note column — the matrix error
|
|
522
|
+
// cells render the same failure messages and go into the same client hand-off.
|
|
523
|
+
const secretValues = inputs.testData ? collectQaEnvSecretDenylist(inputs.testData).values : [];
|
|
516
524
|
|
|
517
525
|
const variantsByVp = new Map<string, CoverageVariant[]>();
|
|
518
526
|
for (const v of variants) {
|
|
@@ -551,7 +559,10 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
|
|
|
551
559
|
priority,
|
|
552
560
|
mode: modes.size > 1 ? 'mixed' : (first?.mode ?? 'auto'),
|
|
553
561
|
layers: Array.from(new Set(groupVariants.flatMap((v) => v.layers))),
|
|
554
|
-
|
|
562
|
+
// The map's own `trace:` codes join the scenarios' `@spec:` ids on the SAME column: a
|
|
563
|
+
// reviewer wants one place to see what an item traces to, whether the id came from the spec
|
|
564
|
+
// or from the analysis. Map codes lead, because they are the ones a person chose for it.
|
|
565
|
+
traces: Array.from(new Set([...(g.trace ?? []), ...groupVariants.flatMap((v) => v.traces)])),
|
|
555
566
|
dimensions: g.dimensions,
|
|
556
567
|
// Only the preconditions COMMON to every variant belong on the parent —
|
|
557
568
|
// copying the first variant's setup mis-states the start state of the
|
|
@@ -612,6 +623,7 @@ export function buildMatrix(inputs: BuildInputs): MatrixModel {
|
|
|
612
623
|
dispositions,
|
|
613
624
|
requirements,
|
|
614
625
|
findings,
|
|
626
|
+
secretValues,
|
|
615
627
|
manifest: {
|
|
616
628
|
unit,
|
|
617
629
|
transformerVersion,
|
|
@@ -53,7 +53,7 @@ function readSpecText(paths: MatrixTargetPaths): string {
|
|
|
53
53
|
: '';
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** apis.yaml endpoint catalog for
|
|
56
|
+
/** apis.yaml endpoint catalog — resolved for EVERY kind (screen/flow read their unit-scoped catalog + the shared one), so hybrid @api preconditions render their real request contract. */
|
|
57
57
|
function catalogFor(paths: MatrixTargetPaths): Record<string, ApiCatalogEntry> {
|
|
58
58
|
return loadApiCatalog(paths.cwd, paths.kind ?? 'screen', paths.area ?? paths.unit);
|
|
59
59
|
}
|
|
@@ -79,6 +79,10 @@ export function loadDeliveryMap(file: string): MapLoadResult {
|
|
|
79
79
|
intent: String(grp.intent ?? ''),
|
|
80
80
|
oracle: String(grp.oracle ?? ''),
|
|
81
81
|
...(grp.dimensions !== undefined ? { dimensions: String(grp.dimensions) } : {}),
|
|
82
|
+
// Free-form by design: these are the project's own analysis codes, not a fixed scheme.
|
|
83
|
+
...(Array.isArray(grp.trace) && grp.trace.length
|
|
84
|
+
? { trace: (grp.trace as unknown[]).map((t) => String(t).trim()).filter(Boolean) }
|
|
85
|
+
: {}),
|
|
82
86
|
category: grp.category as MapCategory,
|
|
83
87
|
review,
|
|
84
88
|
variants: Array.isArray(variants) ? variants.map(String) : [],
|
|
@@ -150,6 +154,7 @@ export function writeDeliveryMap(file: string, map: DeliveryMap): void {
|
|
|
150
154
|
intent: g.intent,
|
|
151
155
|
oracle: g.oracle,
|
|
152
156
|
...(g.dimensions !== undefined ? { dimensions: g.dimensions } : {}),
|
|
157
|
+
...(g.trace && g.trace.length ? { trace: g.trace } : {}),
|
|
153
158
|
category: g.category,
|
|
154
159
|
review: g.review,
|
|
155
160
|
variants: g.variants,
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* and result), so downstream pipelines can re-derive both views from one file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { redactSecrets } from '../api-testcase-formatter';
|
|
9
|
+
import { clipNoteField } from '../playwright-report-parser';
|
|
8
10
|
import { variantState } from './build';
|
|
9
11
|
import { coverageSummary, itemModeLabel, itemResultLabel } from './render-xlsx';
|
|
10
12
|
import { MatrixModel } from './types';
|
|
@@ -75,7 +77,9 @@ export function renderMatrixCsv(model: MatrixModel): string {
|
|
|
75
77
|
v.traces.join(' '),
|
|
76
78
|
variantState(v),
|
|
77
79
|
isoDate(v.result?.startTime),
|
|
78
|
-
|
|
80
|
+
// Redact BEFORE clipping — a clip boundary must never split a secret in
|
|
81
|
+
// a way that lets its prefix through. Same denylist as the Note column.
|
|
82
|
+
v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
|
|
79
83
|
'',
|
|
80
84
|
].map(esc).join(','));
|
|
81
85
|
}
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
BLACK,
|
|
28
28
|
LAVENDER,
|
|
29
29
|
} from '../xlsx-report-builder';
|
|
30
|
+
import { redactSecrets } from '../api-testcase-formatter';
|
|
31
|
+
import { clipNoteField } from '../playwright-report-parser';
|
|
30
32
|
import { variantState } from './build';
|
|
31
33
|
import { DeliveryItem, MatrixModel, MapCategory } from './types';
|
|
32
34
|
|
|
@@ -229,7 +231,8 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
229
231
|
variantState(v),
|
|
230
232
|
isoDate(v.result?.startTime),
|
|
231
233
|
'',
|
|
232
|
-
|
|
234
|
+
// Redact BEFORE clipping — same denylist as the testcases CSV Note column.
|
|
235
|
+
v.result?.error ? clipNoteField(redactSecrets(String(v.result.error), model.secretValues)) : '',
|
|
233
236
|
'',
|
|
234
237
|
];
|
|
235
238
|
vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
|
|
@@ -33,6 +33,17 @@ export interface MapGroup {
|
|
|
33
33
|
* (e.g. "required ×3 · format ×9 · full-width ×2"). Rendered on the parent row
|
|
34
34
|
* instead of concatenating every variant's title — the compact review view. */
|
|
35
35
|
dimensions?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Domain-logic reference ids this item traces to — a decision table, an algorithm, an internal
|
|
38
|
+
* business rule (`DEC-003`, `ALG-002`, `DISC-002`, `BR-001`).
|
|
39
|
+
*
|
|
40
|
+
* `requirements:` already traces to the FR/TR/NFR ids in `requirements/spec.md`. The codes a QA
|
|
41
|
+
* assigns while ANALYSING — which mechanism a case actually exercises — had no field, so they
|
|
42
|
+
* lived as comments in the `.feature` and never reached the Coverage sheet handed to the
|
|
43
|
+
* reviewer: the Trace column came out blank for exactly the items whose traceability mattered
|
|
44
|
+
* most (#651). Optional, and free-form on purpose: these ids are the project's own vocabulary.
|
|
45
|
+
*/
|
|
46
|
+
trace?: string[];
|
|
36
47
|
/** Variant refs: `VP-ID` (all @cases rows when the scenario has a dataset) or `VP-ID#label`. */
|
|
37
48
|
variants: string[];
|
|
38
49
|
}
|
|
@@ -235,6 +246,13 @@ export interface MatrixModel {
|
|
|
235
246
|
manifest: MatrixManifest;
|
|
236
247
|
/** Perf verdict (#569) — present when qa/perf.yaml sets a budget AND the run produced durations. */
|
|
237
248
|
perf?: import('../../harness/perf').PerfVerdict | null;
|
|
249
|
+
/**
|
|
250
|
+
* Resolved ${QA_*} secret values referenced by this unit's test data. Failure
|
|
251
|
+
* messages quote runtime values (a Received: line can echo a typed password),
|
|
252
|
+
* so every error cell the renderers emit must be redacted against this list —
|
|
253
|
+
* the deliverable ships to the client.
|
|
254
|
+
*/
|
|
255
|
+
secretValues: string[];
|
|
238
256
|
}
|
|
239
257
|
|
|
240
258
|
/** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
|
|
@@ -16,14 +16,22 @@ import { PlaywrightResult } from './types';
|
|
|
16
16
|
* Candidate order (first existing wins):
|
|
17
17
|
* 1. <genDir>/<featureBaseName>-test-result.<SUNGEN_ENV>.json (env runs)
|
|
18
18
|
* 2. <genDir>/<featureBaseName>-test-result.json (per-feature reporter)
|
|
19
|
-
* 3. <
|
|
20
|
-
*
|
|
19
|
+
* 3. <genDir>/<featureBaseName>-test-result.<os>[.<SUNGEN_ENV>].json — mobile per-OS
|
|
20
|
+
* reporter files (wdio writes one per capability; a mobile project has NO base file).
|
|
21
|
+
* MOBILE_PLATFORM picks the OS explicitly; otherwise android, then ios.
|
|
22
|
+
* 4. <cwd>/test-results/results.<SUNGEN_ENV>.json
|
|
23
|
+
* 5. <cwd>/test-results/results.json (playwright default)
|
|
21
24
|
*/
|
|
22
25
|
export function resolveResultsPath(cwd: string, genDir: string, featureBaseName: string): string | null {
|
|
23
26
|
const env = process.env.SUNGEN_ENV;
|
|
24
27
|
const candidates: string[] = [];
|
|
25
28
|
if (env) candidates.push(path.join(genDir, `${featureBaseName}-test-result.${env}.json`));
|
|
26
29
|
candidates.push(path.join(genDir, `${featureBaseName}-test-result.json`));
|
|
30
|
+
const mobileOs = (process.env.MOBILE_PLATFORM || '').toLowerCase();
|
|
31
|
+
for (const os of mobileOs === 'android' || mobileOs === 'ios' ? [mobileOs] : ['android', 'ios']) {
|
|
32
|
+
if (env) candidates.push(path.join(genDir, `${featureBaseName}-test-result.${os}.${env}.json`));
|
|
33
|
+
candidates.push(path.join(genDir, `${featureBaseName}-test-result.${os}.json`));
|
|
34
|
+
}
|
|
27
35
|
if (env) candidates.push(path.join(cwd, 'test-results', `results.${env}.json`));
|
|
28
36
|
candidates.push(path.join(cwd, 'test-results', 'results.json'));
|
|
29
37
|
for (const c of candidates) {
|
|
@@ -57,6 +65,9 @@ interface PlaywrightSpec {
|
|
|
57
65
|
duration?: number; // ms — Playwright emits this on every result (#569 perf budgets)
|
|
58
66
|
startTime?: string;
|
|
59
67
|
error?: { message?: string; stack?: string };
|
|
68
|
+
// ALL errors of the attempt. `error` above is only the first one — when a
|
|
69
|
+
// timeout interrupts an assertion, the assertion detail is errors[1+].
|
|
70
|
+
errors?: Array<{ message?: string; stack?: string }>;
|
|
60
71
|
attachments?: Array<{ name?: string; path?: string; contentType?: string }>;
|
|
61
72
|
}>;
|
|
62
73
|
}>;
|
|
@@ -110,7 +121,30 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
|
|
|
110
121
|
const results = test?.results || [];
|
|
111
122
|
const res = results[results.length - 1];
|
|
112
123
|
const status = normalizeStatus(res?.status);
|
|
113
|
-
|
|
124
|
+
// Collect EVERY failure message of the attempt, not just the first: a test
|
|
125
|
+
// timeout wraps the real assertion failure, so `error` alone holds only
|
|
126
|
+
// "Test timeout of 15000ms exceeded." while the Locator/Expected/Received
|
|
127
|
+
// detail QA needs sits in errors[1+]. Stack frames are stripped PER message
|
|
128
|
+
// before joining — otherwise the first message's stack swallows the rest
|
|
129
|
+
// when downstream consumers cut at the first "at " frame.
|
|
130
|
+
const rawMessages = (res?.errors?.length ? res.errors.map((e) => e.message) : [res?.error?.message])
|
|
131
|
+
.map((m) => stripAnsi(m || ''))
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
const distinct: string[] = [];
|
|
134
|
+
const seenKeys = new Set<string>();
|
|
135
|
+
for (const m of rawMessages) {
|
|
136
|
+
const stripped = m.split(/\n\s*at\s/)[0].trim();
|
|
137
|
+
// Key ignores a leading `Error:` — "Error: page closed" and "page closed"
|
|
138
|
+
// are the same failure and must not render as two Note bullets.
|
|
139
|
+
const key = stripped.replace(/^Error:\s*/i, '');
|
|
140
|
+
if (!key || seenKeys.has(key)) continue;
|
|
141
|
+
seenKeys.add(key);
|
|
142
|
+
distinct.push(stripped);
|
|
143
|
+
}
|
|
144
|
+
// Single message: keep the legacy shape (stack intact) so every existing
|
|
145
|
+
// consumer renders byte-identical output. Multiple: join the stack-stripped
|
|
146
|
+
// distinct messages so none can hide behind another's stack.
|
|
147
|
+
const errorMsg = distinct.length > 1 ? distinct.join('\n') : stripAnsi(res?.error?.message || rawMessages[0] || '');
|
|
114
148
|
const trace = res?.attachments?.find((a) => a.name === 'trace' || a.contentType === 'application/zip')?.path;
|
|
115
149
|
|
|
116
150
|
// Strip the leading file-path suite title if present.
|
|
@@ -124,6 +158,7 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
|
|
|
124
158
|
duration: typeof res?.duration === 'number' ? res.duration : undefined,
|
|
125
159
|
startTime: res?.startTime,
|
|
126
160
|
error: errorMsg,
|
|
161
|
+
errorMessages: distinct.length > 0 ? distinct : undefined,
|
|
127
162
|
tracePath: trace,
|
|
128
163
|
projectName: test?.projectName,
|
|
129
164
|
});
|
|
@@ -178,7 +213,7 @@ function normalizeStatus(s: string | undefined): PlaywrightResult['status'] {
|
|
|
178
213
|
* "specs/generated/kudos/kudos.spec.ts > Feature Name > ...",
|
|
179
214
|
* strip that leading segment so matching works against "Feature Name > ...".
|
|
180
215
|
*/
|
|
181
|
-
function stripLeadingFilePathSuite(fullTitle: string): string {
|
|
216
|
+
export function stripLeadingFilePathSuite(fullTitle: string): string {
|
|
182
217
|
const parts = fullTitle.split(' > ');
|
|
183
218
|
if (parts.length > 1 && /\.spec\.ts$/i.test(parts[0])) {
|
|
184
219
|
return parts.slice(1).join(' > ');
|
|
@@ -256,11 +291,23 @@ export function parseExpectedReceived(raw: string): { expected?: string; receive
|
|
|
256
291
|
export function formatNote(result: PlaywrightResult | undefined, expectedFallback = ''): string {
|
|
257
292
|
if (!result) return '';
|
|
258
293
|
if (result.status !== 'failed' && result.status !== 'timedOut') return '';
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
294
|
+
// Per-message: stack frames dropped (frames start with "at ") + a leading `Error:` stripped.
|
|
295
|
+
// Keep newlines inside each message so parseExpectedReceived can delimit Expected/Received.
|
|
296
|
+
const messages = (result.errorMessages?.length ? result.errorMessages : [result.error || ''])
|
|
297
|
+
.map((m) => m.split(/\n\s*at\s/)[0].replace(/^Error:\s*/i, '').trim())
|
|
298
|
+
.filter(Boolean);
|
|
299
|
+
// The message that carries the assertion detail drives Expected/Actual; the other
|
|
300
|
+
// messages (e.g. the "Test timeout of 15000ms exceeded." wrapper) must still be
|
|
301
|
+
// shown — a real assertion failure rendered as a bare timeout reads as flaky.
|
|
302
|
+
// Prefer a message carrying BOTH halves: a half-match (Expected in one message,
|
|
303
|
+
// Received in another) would otherwise put the wrong text in Actual. With
|
|
304
|
+
// expect.soft the first assertion drives the split and the later assertion
|
|
305
|
+
// messages render as `• Error:` bullets — content preserved, columns describe #1.
|
|
306
|
+
const halves = (m: string): number => { const er = parseExpectedReceived(m); return (er.expected ? 1 : 0) + (er.received ? 1 : 0); };
|
|
307
|
+
const assertion = messages.find((m) => halves(m) === 2) ?? messages.find((m) => halves(m) >= 1) ?? messages[0] ?? '';
|
|
308
|
+
const wrappers = messages.filter((m) => m !== assertion);
|
|
309
|
+
const { expected, received } = parseExpectedReceived(assertion);
|
|
310
|
+
const actual = received || clip(assertion); // received value, else the raw error
|
|
264
311
|
|
|
265
312
|
// Newline-separated bullets so each field lands on its own row when the cell wraps — matches the
|
|
266
313
|
// Test Data column's `• key: value` formatting. Excel/CSV honour `\n` inside quoted cells; the
|
|
@@ -276,6 +323,18 @@ export function formatNote(result: PlaywrightResult | undefined, expectedFallbac
|
|
|
276
323
|
else if (lines.length > 1) parts.push(`• Expected result:\n${lines.map((l) => ` ${l}`).join('\n')}`);
|
|
277
324
|
}
|
|
278
325
|
if (actual) parts.push(`• Actual result: ${actual}`);
|
|
326
|
+
// Non-assertion companions of an assertion failure (timeout wrapper etc.), one
|
|
327
|
+
// clipped bullet each so no cell field can blow past the XLSX-safe cap.
|
|
328
|
+
for (const w of wrappers) parts.push(`• Error: ${clip(w)}`);
|
|
279
329
|
if (result.tracePath) parts.push(`• Trace: ${result.tracePath}`);
|
|
280
330
|
return parts.join('\n');
|
|
281
331
|
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* One-cell error rendering for the matrix deliverable: newlines collapsed and
|
|
335
|
+
* clipped to the same cap as Note fields. Replaces ad-hoc `slice(0, 200)` calls
|
|
336
|
+
* that could cut a joined multi-error message mid-assertion.
|
|
337
|
+
*/
|
|
338
|
+
export function clipNoteField(s: string): string {
|
|
339
|
+
return clip(s);
|
|
340
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge two Playwright JSON reports by FULL TEST TITLE — the fix for subset
|
|
3
|
+
* re-runs: Playwright's JSON reporter overwrites the per-target result file,
|
|
4
|
+
* so re-running the 3 failed cases of a 100-case screen used to throw away
|
|
5
|
+
* the 97 passing results (QA then re-ran everything, or asked an AI to merge
|
|
6
|
+
* by hand — non-deterministic).
|
|
7
|
+
*
|
|
8
|
+
* Semantics (deterministic):
|
|
9
|
+
* - same title in both → the NEW spec wins wholesale (its results, its
|
|
10
|
+
* attempts, its timing);
|
|
11
|
+
* - title only in base → kept as-is;
|
|
12
|
+
* - title only in new → appended (its suite structure preserved).
|
|
13
|
+
* Titles compare AFTER stripping the leading file-path suite segment, so a
|
|
14
|
+
* base and a new report generated from different cwd/suite paths still match.
|
|
15
|
+
* `@cases` rows carry a ` — <label>` title suffix and merge as independent
|
|
16
|
+
* entries — re-running one dataset row keeps the other rows.
|
|
17
|
+
*
|
|
18
|
+
* The merge never deletes: a base title missing from the current spec (a
|
|
19
|
+
* scenario renamed by a spec edit) is a GHOST the caller may WARN about, but
|
|
20
|
+
* pruning is not this function's job — the delivery builds rows from the
|
|
21
|
+
* feature, so ghosts never render; only the dead `stats` block would drift,
|
|
22
|
+
* and it is recomputed below.
|
|
23
|
+
*
|
|
24
|
+
* Everything outside suites/specs/tests is passed through untouched from the
|
|
25
|
+
* BASE report (config, metadata, unknown future fields) — tolerance over
|
|
26
|
+
* strictness, the shape belongs to Playwright.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { stripLeadingFilePathSuite } from './playwright-report-parser';
|
|
30
|
+
|
|
31
|
+
interface SpecRef {
|
|
32
|
+
title: string; // stripped full title
|
|
33
|
+
parent: any[]; // the specs array holding the node
|
|
34
|
+
index: number;
|
|
35
|
+
node: any;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MergeOutcome {
|
|
39
|
+
merged: any;
|
|
40
|
+
replaced: string[]; // titles taken from the new report over the base
|
|
41
|
+
added: string[]; // titles only in the new report (appended)
|
|
42
|
+
kept: string[]; // titles only in the base (untouched)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Collect every spec node with its stripped full title and parent array. */
|
|
46
|
+
function collectSpecs(report: any): SpecRef[] {
|
|
47
|
+
const out: SpecRef[] = [];
|
|
48
|
+
const walk = (suites: any[] | undefined, stack: string[]): void => {
|
|
49
|
+
if (!Array.isArray(suites)) return;
|
|
50
|
+
for (const suite of suites) {
|
|
51
|
+
if (!suite || typeof suite !== 'object') continue;
|
|
52
|
+
const nextStack = typeof suite.title === 'string' && suite.title ? [...stack, suite.title] : stack;
|
|
53
|
+
if (Array.isArray(suite.specs)) {
|
|
54
|
+
suite.specs.forEach((spec: any, index: number) => {
|
|
55
|
+
if (!spec || typeof spec.title !== 'string') return;
|
|
56
|
+
const fullTitle = stripLeadingFilePathSuite([...nextStack, spec.title].join(' > '));
|
|
57
|
+
out.push({ title: fullTitle, parent: suite.specs, index, node: spec });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
walk(suite.suites, nextStack);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
walk(report?.suites, []);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Drop consumed specs from a suite tree; prune suites left with nothing. */
|
|
68
|
+
function pruneConsumed(suites: any[], consumed: Set<any>): any[] {
|
|
69
|
+
const kept: any[] = [];
|
|
70
|
+
for (const suite of suites) {
|
|
71
|
+
if (!suite || typeof suite !== 'object') continue;
|
|
72
|
+
if (Array.isArray(suite.specs)) suite.specs = suite.specs.filter((s: any) => !consumed.has(s));
|
|
73
|
+
if (Array.isArray(suite.suites)) suite.suites = pruneConsumed(suite.suites, consumed);
|
|
74
|
+
const hasSpecs = Array.isArray(suite.specs) && suite.specs.length > 0;
|
|
75
|
+
const hasSuites = Array.isArray(suite.suites) && suite.suites.length > 0;
|
|
76
|
+
if (hasSpecs || hasSuites) kept.push(suite);
|
|
77
|
+
}
|
|
78
|
+
return kept;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Recompute the report-level stats counters from the merged tree's final attempts. */
|
|
82
|
+
function recomputeStats(merged: any): void {
|
|
83
|
+
if (!merged.stats || typeof merged.stats !== 'object') return;
|
|
84
|
+
let expected = 0, unexpected = 0, skipped = 0, flaky = 0;
|
|
85
|
+
for (const ref of collectSpecs(merged)) {
|
|
86
|
+
for (const test of ref.node.tests ?? []) {
|
|
87
|
+
const results = test?.results ?? [];
|
|
88
|
+
const last = results[results.length - 1];
|
|
89
|
+
const status = last?.status;
|
|
90
|
+
if (status === 'passed') { if (results.some((r: any) => r?.status === 'failed' || r?.status === 'timedOut')) flaky++; else expected++; }
|
|
91
|
+
else if (status === 'skipped') skipped++;
|
|
92
|
+
else if (status) unexpected++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
merged.stats = { ...merged.stats, expected, unexpected, skipped, flaky };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Merge `next` into a DEEP COPY of `base` (neither input is mutated).
|
|
100
|
+
* Throws on inputs that are not report-shaped objects.
|
|
101
|
+
*/
|
|
102
|
+
export function mergeResultsReports(base: any, next: any): MergeOutcome {
|
|
103
|
+
for (const [label, r] of [['base', base], ['new', next]] as const) {
|
|
104
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) {
|
|
105
|
+
throw new Error(`results merge: the ${label} report is not a JSON object.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const merged = JSON.parse(JSON.stringify(base));
|
|
109
|
+
const nextCopy = JSON.parse(JSON.stringify(next));
|
|
110
|
+
|
|
111
|
+
// Last occurrence wins within one report — the same tie-break walkSuites uses.
|
|
112
|
+
const newByTitle = new Map<string, SpecRef>();
|
|
113
|
+
for (const ref of collectSpecs(nextCopy)) newByTitle.set(ref.title, ref);
|
|
114
|
+
|
|
115
|
+
const replaced: string[] = [];
|
|
116
|
+
const kept: string[] = [];
|
|
117
|
+
const consumed = new Set<any>();
|
|
118
|
+
for (const ref of collectSpecs(merged)) {
|
|
119
|
+
const incoming = newByTitle.get(ref.title);
|
|
120
|
+
if (incoming) {
|
|
121
|
+
ref.parent[ref.index] = incoming.node; // the new spec wins wholesale
|
|
122
|
+
consumed.add(incoming.node);
|
|
123
|
+
replaced.push(ref.title);
|
|
124
|
+
} else {
|
|
125
|
+
kept.push(ref.title);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const added: string[] = [];
|
|
130
|
+
for (const [title, ref] of newByTitle) {
|
|
131
|
+
if (!consumed.has(ref.node)) added.push(title);
|
|
132
|
+
}
|
|
133
|
+
if (added.length > 0 && Array.isArray(nextCopy.suites)) {
|
|
134
|
+
merged.suites = [...(Array.isArray(merged.suites) ? merged.suites : []), ...pruneConsumed(nextCopy.suites, consumed)];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
recomputeStats(merged);
|
|
138
|
+
return { merged, replaced, added, kept };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Ghost detection for the WARN (never a prune): base-kept titles that no
|
|
143
|
+
* longer correspond to a CURRENT spec title. A `@cases` report title carries
|
|
144
|
+
* a ` — <label>` suffix while the spec title is the base form, so a ghost is
|
|
145
|
+
* a title that neither equals a spec title nor extends one with that suffix.
|
|
146
|
+
*/
|
|
147
|
+
export function findGhostTitles(keptTitles: string[], specTitles: string[]): string[] {
|
|
148
|
+
return keptTitles.filter((title) =>
|
|
149
|
+
!specTitles.some((spec) => title === spec || title.startsWith(`${spec} — `)));
|
|
150
|
+
}
|