@sun-asterisk/sungen 3.2.24-beta.6 → 3.2.25
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/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 +10 -0
- 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/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 +7 -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/biometric-action.hbs +9 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
- 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/deep-link-action.hbs +8 -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/actions/location-services-action.hbs +11 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.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/not-checked-assertion.hbs +6 -4
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -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/steps/gestures/pull-to-refresh-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +302 -29
- 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 +19 -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 +24 -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.map +1 -1
- package/dist/harness/audit.js +7 -5
- 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 +14 -4
- 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/parse.d.ts +1 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +6 -1
- package/dist/harness/parse.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/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +6 -2
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +5 -1
- package/dist/harness/sensors.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 +25 -4
- 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/mobile-runtime-scaffolder.d.ts.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.js +5 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
- 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/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 +120 -10
- 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 +31 -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 +9 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/dist/orchestrator/templates/env.appium.example +21 -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-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +79 -6
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +70 -7
- 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 +126 -10
- package/dist/utils/selector-types.d.ts +1 -1
- package/dist/utils/selector-types.d.ts.map +1 -1
- package/dist/utils/selector-types.js +1 -0
- package/dist/utils/selector-types.js.map +1 -1
- package/package.json +8 -6
- package/src/capabilities/context-router.ts +13 -5
- 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 +9 -0
- package/src/exporters/matrix/export.ts +1 -1
- package/src/exporters/matrix/render-csv.ts +5 -1
- package/src/exporters/matrix/render-xlsx.ts +4 -1
- package/src/exporters/matrix/types.ts +7 -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/biometric-action.hbs +9 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +11 -6
- 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/deep-link-action.hbs +8 -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/actions/location-services-action.hbs +11 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +21 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/restart-app-action.hbs +16 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/set-connectivity-action.hbs +11 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +9 -5
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +10 -4
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/device-log-assertion.hbs +16 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disappears-within-assertion.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/not-checked-assertion.hbs +6 -4
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +4 -3
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/still-visible-after-assertion.hbs +4 -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/steps/gestures/pull-to-refresh-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/swipe-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/appium-selector.hbs +7 -1
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +302 -29
- 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 +22 -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 +26 -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 +7 -5
- package/src/harness/capability-plan.ts +13 -6
- package/src/harness/capability.ts +16 -0
- package/src/harness/catalog/drivers.yaml +5 -0
- package/src/harness/parse.ts +7 -1
- package/src/harness/run-manifest.ts +254 -0
- package/src/harness/script-check.ts +6 -2
- package/src/harness/sensors.ts +5 -1
- package/src/harness/viewpoint-ledger.ts +26 -5
- package/src/index.ts +1 -0
- package/src/orchestrator/lockstep-version.ts +46 -0
- package/src/orchestrator/mobile-runtime-scaffolder.ts +5 -1
- package/src/orchestrator/project-deps-sync.ts +175 -0
- package/src/orchestrator/project-initializer.ts +5 -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 +120 -10
- 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 +31 -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 +9 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/src/orchestrator/templates/env.appium.example +21 -0
- package/src/orchestrator/templates/specs-api.ts +214 -11
- package/src/orchestrator/templates/specs-db.ts +70 -7
- 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 +126 -10
- package/src/utils/selector-types.ts +1 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sungen changed` — which scenarios does the current edit oblige a re-run of?
|
|
3
|
+
*
|
|
4
|
+
* Backs `run-test --change-only`. The selection is computed here rather than in the AI workflow so
|
|
5
|
+
* it is deterministic and testable: the same edit always yields the same list, and the list can be
|
|
6
|
+
* asserted in CI. The workflow's job is only to run it, merge, and record.
|
|
7
|
+
*
|
|
8
|
+
* Bias: when an edit cannot be attributed to specific scenarios, EVERY scenario is selected and the
|
|
9
|
+
* output says so. Over-running costs minutes; under-running reports a scenario green that never
|
|
10
|
+
* re-ran after the change that broke it.
|
|
11
|
+
*/
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { buildRunManifest, readRunManifest, writeRunManifest, selectChanged } from '../../harness/run-manifest';
|
|
14
|
+
import { findUnitDir } from '../resolve-unit';
|
|
15
|
+
import { execFileSync } from 'child_process';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run-affecting files git reports changed for this unit, or [] when git has no usable opinion.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately narrow and fail-quiet:
|
|
22
|
+
* - not a repo, or the unit is UNTRACKED → [] (git knows nothing; the dogfood project is exactly
|
|
23
|
+
* this case, and treating "untracked" as "changed" would widen on every single run);
|
|
24
|
+
* - only `features/`, `selectors/`, `test-data/` count — a spec.md or screenshot edit is
|
|
25
|
+
* regeneration territory (`sungen manifest --diff`), not a reason to re-run;
|
|
26
|
+
* - `diff HEAD` so staged and unstaged edits both count.
|
|
27
|
+
*/
|
|
28
|
+
function gitChangedFiles(unitDir: string): string[] {
|
|
29
|
+
const rel = path.relative(process.cwd(), unitDir) || '.';
|
|
30
|
+
const git = (args: string[]): string =>
|
|
31
|
+
execFileSync('git', args, { cwd: process.cwd(), encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
32
|
+
try {
|
|
33
|
+
if (!git(['ls-files', rel]).trim()) return []; // untracked unit → git has no baseline
|
|
34
|
+
return git(['diff', 'HEAD', '--name-only', '--', rel])
|
|
35
|
+
.split('\n')
|
|
36
|
+
.map((l) => l.trim())
|
|
37
|
+
.filter((l) => /\/(features|selectors|test-data)\//.test(l));
|
|
38
|
+
} catch {
|
|
39
|
+
return []; // no git, no repo, detached weirdness — the fingerprint still decides
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Escape a scenario title for a `-g` / `--mochaOpts.grep` regex. */
|
|
44
|
+
const toGrep = (titles: string[]): string =>
|
|
45
|
+
titles.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
46
|
+
|
|
47
|
+
export function registerChangedCommand(program: Command): void {
|
|
48
|
+
program
|
|
49
|
+
.command('changed')
|
|
50
|
+
.description('List the scenarios a selectors/test-data/feature edit obliges you to re-run (backs run-test --change-only)')
|
|
51
|
+
.option('-s, --screen <name>', 'Screen or flow name')
|
|
52
|
+
.option('--api <name>', 'API-first area or api flow (qa/api/<name>)')
|
|
53
|
+
.option('--area <name>', 'Alias of --api')
|
|
54
|
+
.option('--json', 'Output the raw selection as JSON')
|
|
55
|
+
.option('--grep', 'Output ONLY a regex of the selected titles, for -g / --mochaOpts.grep')
|
|
56
|
+
.option('--record', 'Record the current inputs as the new baseline (run this AFTER a successful run)')
|
|
57
|
+
.action((options) => {
|
|
58
|
+
try {
|
|
59
|
+
const name = options.screen || options.api || options.area;
|
|
60
|
+
if (!name) throw new Error('Provide --screen <name> (or --api <area>)');
|
|
61
|
+
const dir = findUnitDir(name);
|
|
62
|
+
if (!dir) throw new Error(`Not found: qa/screens|flows|api/${name}`);
|
|
63
|
+
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
const after = buildRunManifest(cwd, dir, name);
|
|
66
|
+
|
|
67
|
+
if (options.record) {
|
|
68
|
+
const p = writeRunManifest(cwd, name, after);
|
|
69
|
+
console.log(`✓ recorded run baseline for ${name} → ${p.replace(`${cwd}/`, '')} (${Object.keys(after.scenarios).length} scenarios)`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const selection = selectChanged(readRunManifest(cwd, name), after, gitChangedFiles(dir));
|
|
74
|
+
|
|
75
|
+
if (options.grep) {
|
|
76
|
+
// Empty output is meaningful: nothing to run. A caller must treat "" as "skip the run",
|
|
77
|
+
// never as "match everything" — an empty grep would run the whole suite.
|
|
78
|
+
console.log(toGrep(selection.scenarios));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (options.json) { console.log(JSON.stringify(selection, null, 2)); return; }
|
|
82
|
+
|
|
83
|
+
const { total, selected, firstRun } = selection.summary;
|
|
84
|
+
console.log(`\n━━━ Change selection: ${name} ━━━`);
|
|
85
|
+
if (!selected) {
|
|
86
|
+
console.log(` ✓ Nothing changed since the last recorded run — ${total} scenario(s) still current.\n`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (selection.all) {
|
|
90
|
+
console.log(` ⚠ ALL ${total} scenario(s) selected — ${selection.reasons[selection.scenarios[0]]}.`);
|
|
91
|
+
console.log(firstRun
|
|
92
|
+
? ' Record a baseline after this run so the next one can narrow: sungen changed --screen ' + name + ' --record\n'
|
|
93
|
+
: ' An unattributable edit widens to the whole unit on purpose — a missed scenario would report green without re-running.\n');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(` ${selected} of ${total} scenario(s) need a re-run:\n`);
|
|
97
|
+
for (const t of selection.scenarios) console.log(` ↻ ${t}\n ${selection.reasons[t]}`);
|
|
98
|
+
console.log('');
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -436,7 +436,8 @@ async function exportTarget(
|
|
|
436
436
|
const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : '';
|
|
437
437
|
const explicitEnv = process.env.SUNGEN_ENV;
|
|
438
438
|
|
|
439
|
-
//
|
|
439
|
+
// Load the endpoint catalog for every kind: api-kind gets the API detail sheet;
|
|
440
|
+
// screen/flow rows name their @api precondition endpoints from it.
|
|
440
441
|
const apiCatalog = loadApiCatalog(cwd, target.kind, target.screen);
|
|
441
442
|
const hasApiCatalog = target.kind === 'api' && Object.keys(apiCatalog).length > 0;
|
|
442
443
|
|
|
@@ -672,8 +673,21 @@ async function exportMatrixTarget(
|
|
|
672
673
|
cwd: string,
|
|
673
674
|
target: DeliveryTarget,
|
|
674
675
|
opts: { check?: boolean; preview?: boolean; format?: MatrixFormat },
|
|
675
|
-
): Promise<MatrixRunSummary | null> {
|
|
676
|
+
): Promise<MatrixRunSummary | 'skipped' | null> {
|
|
676
677
|
const paths = matrixPathsFor(cwd, target);
|
|
678
|
+
|
|
679
|
+
// A feature whose scenarios are ALL @exploration probes has nothing to deliver —
|
|
680
|
+
// skip it quietly (no map required) instead of demanding a reviewed grouping for
|
|
681
|
+
// probes that are, by contract, outside the official suite.
|
|
682
|
+
{
|
|
683
|
+
const featureMeta = parseFeatureMetadata(paths.featureFile);
|
|
684
|
+
const deliverable = featureMeta.scenarios.filter((s) => !s.tags.some((t) => /^@exploration\b/i.test(t)));
|
|
685
|
+
if (featureMeta.scenarios.length > 0 && deliverable.length === 0) {
|
|
686
|
+
log(`${COLOR.gray}${paths.label}: nothing to deliver — all ${featureMeta.scenarios.length} scenario(s) are @exploration probes (outside the official suite).${COLOR.reset}`);
|
|
687
|
+
return 'skipped';
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
677
691
|
const { model, mapMissing, mapErrors } = loadMatrixModel(paths);
|
|
678
692
|
|
|
679
693
|
if (mapMissing) {
|
|
@@ -937,6 +951,16 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
937
951
|
let blocked = false;
|
|
938
952
|
for (const target of toExport) {
|
|
939
953
|
const paths = matrixPathsFor(cwd, target);
|
|
954
|
+
// Same contract as the export path: an all-@exploration feature has no
|
|
955
|
+
// deliverable variants, so there is no map to approve — skip, not an error.
|
|
956
|
+
{
|
|
957
|
+
const featureMeta = parseFeatureMetadata(paths.featureFile);
|
|
958
|
+
if (featureMeta.scenarios.length > 0
|
|
959
|
+
&& featureMeta.scenarios.every((s) => s.tags.some((t) => /^@exploration\b/i.test(t)))) {
|
|
960
|
+
log(`${COLOR.gray}${paths.label}: nothing to approve — all scenario(s) are @exploration probes.${COLOR.reset}`);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
940
964
|
try {
|
|
941
965
|
const { findings, approved } = approveMatrix(paths, ids);
|
|
942
966
|
if (approved.length === 0) {
|
|
@@ -961,6 +985,7 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
961
985
|
const s = await exportMatrixTarget(cwd, target, {
|
|
962
986
|
check: options.check, preview: options.preview, format: format as MatrixFormat,
|
|
963
987
|
});
|
|
988
|
+
if (s === 'skipped') continue; // all-@exploration feature — neither exported nor a failure
|
|
964
989
|
if (s) matrixSummaries.push(s);
|
|
965
990
|
else notExported.push(target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName);
|
|
966
991
|
}
|
|
@@ -200,7 +200,7 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
200
200
|
console.log(`\n⚠️ Compile diagnostics (${diagnostics.count}):`);
|
|
201
201
|
for (const line of diagnostics.formatLines().slice(0, 20)) console.log(` ${line}`);
|
|
202
202
|
if (diagnostics.count > 20) console.log(` … and ${diagnostics.count - 20} more`);
|
|
203
|
-
console.log(`
|
|
203
|
+
console.log(` Unsupported steps compile to a runtime throw — the test fails loudly instead of passing having checked nothing.`);
|
|
204
204
|
if (options.strict) {
|
|
205
205
|
console.error(`\n--strict: failing generation on ${diagnostics.count} diagnostic(s).`);
|
|
206
206
|
process.exit(1);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sungen results merge` — fold a subset re-run's Playwright JSON report into
|
|
3
|
+
* the existing per-target result file instead of overwriting it. See
|
|
4
|
+
* exporters/results-merge.ts for the title-keyed semantics; this command owns
|
|
5
|
+
* path resolution, validation, ghost warnings, and the atomic write.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { mergeResultsReports, findGhostTitles } from '../../exporters/results-merge';
|
|
12
|
+
import { resolveResultsPath } from '../../exporters/playwright-report-parser';
|
|
13
|
+
import { parseSpecFile } from '../../exporters/spec-parser';
|
|
14
|
+
|
|
15
|
+
interface MergeCliOptions {
|
|
16
|
+
out?: string;
|
|
17
|
+
screen?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readReport(file: string, label: string): any {
|
|
21
|
+
if (!fs.existsSync(file)) {
|
|
22
|
+
throw new Error(`results merge: ${label} file not found: ${file}`);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error(`results merge: ${label} file is not valid JSON (${file}): ${err instanceof Error ? err.message : String(err)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the generated dir that owns FEATURE `name` — by its spec file, not by
|
|
33
|
+
* unit-dir name: a multi-feature screen keeps `home.spec.ts` AND
|
|
34
|
+
* `home-modal.spec.ts` in specs/generated/home/, and each feature has its OWN
|
|
35
|
+
* per-target result file. Kind order mirrors delivery's target resolution
|
|
36
|
+
* (flows → screens → api → api flows); the same basename found in more than one
|
|
37
|
+
* place is an ambiguity error, never a silent pick.
|
|
38
|
+
*/
|
|
39
|
+
function resolveFeatureDir(cwd: string, name: string): string {
|
|
40
|
+
const roots = [
|
|
41
|
+
path.join(cwd, 'specs', 'generated', 'flows'),
|
|
42
|
+
path.join(cwd, 'specs', 'generated'),
|
|
43
|
+
path.join(cwd, 'specs', 'generated', 'api'),
|
|
44
|
+
path.join(cwd, 'specs', 'generated', 'api', 'flows'),
|
|
45
|
+
];
|
|
46
|
+
const hits: string[] = [];
|
|
47
|
+
for (const root of roots) {
|
|
48
|
+
if (!fs.existsSync(root)) continue;
|
|
49
|
+
for (const unit of fs.readdirSync(root)) {
|
|
50
|
+
const dir = path.join(root, unit);
|
|
51
|
+
// the middle root also contains flows/ and api/ themselves — units only
|
|
52
|
+
if (['flows', 'api'].includes(unit) && root.endsWith(path.join('specs', 'generated'))) continue;
|
|
53
|
+
if (fs.existsSync(path.join(dir, `${name}.spec.ts`)) && !hits.includes(dir)) hits.push(dir);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (hits.length === 0) {
|
|
57
|
+
throw new Error(`results merge: no generated spec named "${name}.spec.ts" found under specs/generated/ — pass the feature basename (a multi-feature screen has one result file PER feature), and run sungen generate first.`);
|
|
58
|
+
}
|
|
59
|
+
if (hits.length > 1) {
|
|
60
|
+
throw new Error(`results merge: "${name}" is ambiguous — found in ${hits.map((h) => path.relative(cwd, h)).join(' and ')}. Use the explicit two-file form.`);
|
|
61
|
+
}
|
|
62
|
+
return hits[0];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Runs the merge. Exported for tests; the commander wiring below is a thin
|
|
67
|
+
* shell around it. Returns the path written.
|
|
68
|
+
*/
|
|
69
|
+
export function runResultsMerge(baseArg: string | undefined, newArg: string | undefined, opts: MergeCliOptions, cwd: string = process.cwd()): string {
|
|
70
|
+
let baseFile = baseArg;
|
|
71
|
+
let newFile = newArg;
|
|
72
|
+
let specTitles: string[] | null = null;
|
|
73
|
+
|
|
74
|
+
if (opts.screen) {
|
|
75
|
+
// Convenience form: `results merge --screen <name> <fresh.json>` — <name> is
|
|
76
|
+
// the FEATURE basename; its per-target file lives next to its spec, with the
|
|
77
|
+
// same in-dir candidate order delivery reads.
|
|
78
|
+
if (baseArg && newArg) throw new Error('results merge: --screen takes ONE report file (the fresh subset run).');
|
|
79
|
+
newFile = baseArg ?? newArg;
|
|
80
|
+
if (!newFile) throw new Error('results merge: --screen needs the fresh subset report file as the argument.');
|
|
81
|
+
const dir = resolveFeatureDir(cwd, opts.screen);
|
|
82
|
+
baseFile = resolveResultsPath(cwd, dir, opts.screen);
|
|
83
|
+
// resolveResultsPath falls back to the cwd-global test-results/results.json —
|
|
84
|
+
// a file shared by EVERY unit. Merging one unit's subset into it would report
|
|
85
|
+
// success while delivery keeps reading the stale per-target file: refuse.
|
|
86
|
+
if (!baseFile || path.dirname(baseFile) !== dir) {
|
|
87
|
+
throw new Error(`results merge: no per-target result file for "${opts.screen}" in ${path.relative(cwd, dir)} — nothing to merge into (a first full run writes it directly).`);
|
|
88
|
+
}
|
|
89
|
+
// Ghost/foreign-title checks need the CURRENT spec titles.
|
|
90
|
+
const specFile = path.join(dir, `${opts.screen}.spec.ts`);
|
|
91
|
+
try { specTitles = parseSpecFile(specFile).tests.map((t) => t.testTitle); } catch { specTitles = null; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!baseFile || !newFile) {
|
|
95
|
+
throw new Error('results merge: expected <base.json> <new.json>, or --screen <name> <new.json>.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const base = readReport(baseFile, 'base');
|
|
99
|
+
const fresh = readReport(newFile, 'new');
|
|
100
|
+
const { merged, replaced, added, kept } = mergeResultsReports(base, fresh);
|
|
101
|
+
|
|
102
|
+
if (specTitles) {
|
|
103
|
+
// A fresh report whose ADDED titles are unknown to this feature's spec was
|
|
104
|
+
// produced by a DIFFERENT feature — writing it here would silently corrupt
|
|
105
|
+
// this file while the right one stays stale. Refuse before writing.
|
|
106
|
+
const foreign = findGhostTitles(added, specTitles);
|
|
107
|
+
if (foreign.length > 0) {
|
|
108
|
+
throw new Error(`results merge: ${foreign.length} title(s) in ${path.basename(newFile)} match no scenario of "${opts.screen}" (e.g. ${foreign[0]}) — is this another feature's report? Base file untouched.`);
|
|
109
|
+
}
|
|
110
|
+
const ghosts = findGhostTitles(kept, specTitles);
|
|
111
|
+
if (ghosts.length > 0) {
|
|
112
|
+
console.warn(`⚠ ${ghosts.length} result title(s) no longer match any current scenario (renamed by a spec edit?) — kept, not deleted:`);
|
|
113
|
+
for (const g of ghosts.slice(0, 10)) console.warn(` - ${g}`);
|
|
114
|
+
if (ghosts.length > 10) console.warn(` … and ${ghosts.length - 10} more`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Atomic write: a crash mid-write must never leave a truncated result file —
|
|
119
|
+
// that file is the single source of execution truth for delivery/dashboard.
|
|
120
|
+
// (Same-dir rename = same device; concurrent merges are last-rename-wins.)
|
|
121
|
+
const outFile = opts.out ?? baseFile;
|
|
122
|
+
const tmp = `${outFile}.tmp-${process.pid}`;
|
|
123
|
+
try {
|
|
124
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2));
|
|
125
|
+
fs.renameSync(tmp, outFile);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup */ }
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`✓ merged ${path.basename(newFile)} into ${path.relative(cwd, outFile)} — ${replaced.length} updated, ${added.length} added, ${kept.length} kept`);
|
|
132
|
+
return outFile;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function registerResultsCommand(program: Command): void {
|
|
136
|
+
const results = program
|
|
137
|
+
.command('results')
|
|
138
|
+
.description('Operate on per-target Playwright result files');
|
|
139
|
+
|
|
140
|
+
results
|
|
141
|
+
.command('merge [base] [new]')
|
|
142
|
+
.description('Merge a subset re-run\'s report into an existing result file (same title → new wins; others kept)')
|
|
143
|
+
.option('-o, --out <file>', 'write the merged report here instead of overwriting the base file')
|
|
144
|
+
.option('--screen <name>', 'resolve the base file from this FEATURE basename (a multi-feature screen has one result file per feature); pass only the fresh report as the argument')
|
|
145
|
+
.action((baseArg: string | undefined, newArg: string | undefined, opts: MergeCliOptions) => {
|
|
146
|
+
try {
|
|
147
|
+
runResultsMerge(baseArg, newArg, opts);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { spawnSync } from 'child_process';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { coreVersion } from '../../orchestrator/lockstep-version';
|
|
6
|
+
import { syncProjectSungenDeps } from '../../orchestrator/project-deps-sync';
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* `sungen update` does two jobs in sequence:
|
|
@@ -7,8 +11,12 @@ import { spawnSync } from 'child_process';
|
|
|
7
11
|
* 1. Reinstall `@sun-asterisk/sungen@latest` globally via npm so the bundled
|
|
8
12
|
* AI templates are refreshed.
|
|
9
13
|
* 2. Re-execute `sungen update` with the env var `SUNGEN_UPDATE_SKIP_NPM=1`
|
|
10
|
-
* so the
|
|
11
|
-
*
|
|
14
|
+
* so the *new* binary (a) reconciles the project's own sungen packages onto
|
|
15
|
+
* its version line and (b) overwrites the AI rules / commands / skills
|
|
16
|
+
* inside the project from the new templates.
|
|
17
|
+
*
|
|
18
|
+
* Step 2 runs under the NEW binary on purpose: the version the project should be
|
|
19
|
+
* pinned to is the one doing the pinning, so the two can never disagree.
|
|
12
20
|
*
|
|
13
21
|
* We use an environment variable (not a CLI flag) for the internal hand-off
|
|
14
22
|
* because Commander throws "unknown option" if the currently-installed
|
|
@@ -23,6 +31,12 @@ import { spawnSync } from 'child_process';
|
|
|
23
31
|
|
|
24
32
|
const SKIP_NPM_ENV = 'SUNGEN_UPDATE_SKIP_NPM';
|
|
25
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Carried to the re-exec as an env var for the same reason as SKIP_NPM_ENV: a flag would crash an
|
|
36
|
+
* older published binary with "unknown option", an env var is ignored by it.
|
|
37
|
+
*/
|
|
38
|
+
const SKIP_PROJECT_ENV = 'SUNGEN_UPDATE_SKIP_PROJECT';
|
|
39
|
+
|
|
26
40
|
export function registerUpdateCommand(program: Command): void {
|
|
27
41
|
program
|
|
28
42
|
.command('update')
|
|
@@ -48,13 +62,20 @@ export function registerUpdateCommand(program: Command): void {
|
|
|
48
62
|
)
|
|
49
63
|
.option(
|
|
50
64
|
'--skip-npm-install',
|
|
51
|
-
'Skip the npm install step (refresh project AI assets only)',
|
|
65
|
+
'Skip the global npm install step (refresh project AI assets only)',
|
|
66
|
+
false,
|
|
67
|
+
)
|
|
68
|
+
.option(
|
|
69
|
+
'--skip-project',
|
|
70
|
+
'Skip reinstalling the project-local sungen packages (AI assets only — leaves node_modules stale)',
|
|
52
71
|
false,
|
|
53
72
|
)
|
|
54
|
-
.action(async (options: { dryRun?: boolean; beta?: boolean; tag?: string; skipNpmInstall?: boolean; assistant?: string[]; force?: boolean }) => {
|
|
73
|
+
.action(async (options: { dryRun?: boolean; beta?: boolean; tag?: string; skipNpmInstall?: boolean; skipProject?: boolean; assistant?: string[]; force?: boolean }) => {
|
|
55
74
|
try {
|
|
56
75
|
const skipNpm =
|
|
57
76
|
Boolean(options.skipNpmInstall) || process.env[SKIP_NPM_ENV] === '1';
|
|
77
|
+
const skipProject =
|
|
78
|
+
Boolean(options.skipProject) || process.env[SKIP_PROJECT_ENV] === '1';
|
|
58
79
|
|
|
59
80
|
// Channel resolution via npm dist-tags. Default `latest` = official/stable.
|
|
60
81
|
// `--beta` → beta channel; running plain `sungen update` later switches back to stable.
|
|
@@ -63,10 +84,18 @@ export function registerUpdateCommand(program: Command): void {
|
|
|
63
84
|
if (!skipNpm) {
|
|
64
85
|
reinstallSungen(channel);
|
|
65
86
|
printCurrentVersion(channel);
|
|
66
|
-
reExecUpdateForAIAssets(options.dryRun ?? false, options.assistant ?? [], options.force ?? false);
|
|
87
|
+
reExecUpdateForAIAssets(options.dryRun ?? false, options.assistant ?? [], options.force ?? false, skipProject);
|
|
67
88
|
return;
|
|
68
89
|
}
|
|
69
90
|
|
|
91
|
+
// Running under the NEW binary now: pin the project's own copies to THIS version before
|
|
92
|
+
// refreshing the AI assets, so the compiler the project runs matches the templates it gets.
|
|
93
|
+
if (skipProject) {
|
|
94
|
+
console.log('⏭ Skipping project-local package sync (--skip-project).\n');
|
|
95
|
+
} else {
|
|
96
|
+
syncProjectSungenDeps(process.cwd(), coreVersion(), options.dryRun ?? false);
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
const { resolveAssistants } = require('../../orchestrator/ai-skills/resolve-assistants');
|
|
71
100
|
const { AIRulesUpdater } = require('../../orchestrator/ai-rules-updater');
|
|
72
101
|
const updater = new AIRulesUpdater(process.cwd());
|
|
@@ -94,8 +123,15 @@ function reinstallSungen(channel: string): void {
|
|
|
94
123
|
}
|
|
95
124
|
|
|
96
125
|
function printCurrentVersion(channel: string): void {
|
|
97
|
-
console.log('\n🔎 Installed version:');
|
|
98
|
-
|
|
126
|
+
console.log('\n🔎 Installed version (global):');
|
|
127
|
+
// Same PATH caveat as the re-exec: report the version that was just installed globally, not a
|
|
128
|
+
// project-local copy that happens to shadow it.
|
|
129
|
+
const entry = globalCoreEntry();
|
|
130
|
+
if (entry) {
|
|
131
|
+
spawnSync(process.execPath, [entry, '--version'], { stdio: 'inherit' });
|
|
132
|
+
} else {
|
|
133
|
+
spawnSync('sungen', ['--version'], { stdio: 'inherit', shell: true });
|
|
134
|
+
}
|
|
99
135
|
if (channel === 'latest') {
|
|
100
136
|
console.log(' Channel: stable. To try prereleases: sungen update --beta');
|
|
101
137
|
} else {
|
|
@@ -104,17 +140,40 @@ function printCurrentVersion(channel: string): void {
|
|
|
104
140
|
console.log('');
|
|
105
141
|
}
|
|
106
142
|
|
|
107
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Absolute path to the globally installed core's CLI entry, or null when it can't be located
|
|
145
|
+
* (then the caller falls back to a PATH lookup). Asks npm for the global root instead of guessing
|
|
146
|
+
* a prefix, so nvm / Volta / Windows / custom-prefix installs all resolve correctly.
|
|
147
|
+
*/
|
|
148
|
+
function globalCoreEntry(): string | null {
|
|
149
|
+
try {
|
|
150
|
+
const out = spawnSync('npm', ['root', '-g'], { encoding: 'utf-8', shell: true });
|
|
151
|
+
const root = out.status === 0 ? (out.stdout || '').trim() : '';
|
|
152
|
+
if (!root) return null;
|
|
153
|
+
const entry = path.join(root, '@sun-asterisk', 'sungen', 'bin', 'sungen.js');
|
|
154
|
+
return fs.existsSync(entry) ? entry : null;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function reExecUpdateForAIAssets(dryRun: boolean, assistants: string[], force: boolean, skipProject: boolean): void {
|
|
108
161
|
const args = ['update'];
|
|
109
162
|
if (dryRun) args.push('--dry-run');
|
|
110
163
|
if (force) args.push('--force');
|
|
111
164
|
for (const a of assistants) args.push('--assistant', a);
|
|
112
165
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
166
|
+
const env: NodeJS.ProcessEnv = { ...process.env, [SKIP_NPM_ENV]: '1' };
|
|
167
|
+
if (skipProject) env[SKIP_PROJECT_ENV] = '1';
|
|
168
|
+
|
|
169
|
+
// Run the JUST-INSTALLED global entry directly rather than whatever `sungen` resolves to on PATH.
|
|
170
|
+
// Inside a project, `npx sungen update` / `npm run` put the project's own bin dir FIRST on PATH, so
|
|
171
|
+
// a PATH lookup here re-executes the OLD local copy — step 2 would then refresh nothing, which is
|
|
172
|
+
// exactly the "update ran but the project didn't change" failure this flow exists to prevent.
|
|
173
|
+
const entry = globalCoreEntry();
|
|
174
|
+
const result = entry
|
|
175
|
+
? spawnSync(process.execPath, [entry, ...args], { stdio: 'inherit', env })
|
|
176
|
+
: spawnSync('sungen', args, { stdio: 'inherit', shell: true, env });
|
|
118
177
|
|
|
119
178
|
if (result.status !== 0) {
|
|
120
179
|
console.error(
|
package/src/cli/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { registerGateCommand } from './commands/gate';
|
|
|
22
22
|
import { registerIngestCommand } from './commands/ingest';
|
|
23
23
|
import { registerEvalCommand } from './commands/eval';
|
|
24
24
|
import { registerManifestCommand } from './commands/manifest';
|
|
25
|
+
import { registerChangedCommand } from './commands/changed';
|
|
25
26
|
import { registerLedgerCommand } from './commands/ledger';
|
|
26
27
|
import { registerFeedbackCommand } from './commands/feedback';
|
|
27
28
|
import { registerQaFeedbackCommand } from './commands/qa-feedback';
|
|
@@ -34,6 +35,7 @@ import { registerCapabilityCommand } from './commands/capability';
|
|
|
34
35
|
import { registerFlowCheckCommand } from './commands/flow-check';
|
|
35
36
|
import { registerContextCommand } from './commands/context';
|
|
36
37
|
import { registerRepairCommand } from './commands/repair';
|
|
38
|
+
import { registerResultsCommand } from './commands/results-merge';
|
|
37
39
|
import { registerLoginCommand } from './commands/login';
|
|
38
40
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
39
41
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
@@ -70,6 +72,7 @@ async function main() {
|
|
|
70
72
|
registerJourneyCommand(program);
|
|
71
73
|
registerGateCommand(program);
|
|
72
74
|
registerManifestCommand(program);
|
|
75
|
+
registerChangedCommand(program);
|
|
73
76
|
registerLedgerCommand(program);
|
|
74
77
|
registerFeedbackCommand(program);
|
|
75
78
|
registerQaFeedbackCommand(program);
|
|
@@ -82,6 +85,7 @@ async function main() {
|
|
|
82
85
|
registerFlowCheckCommand(program);
|
|
83
86
|
registerContextCommand(program);
|
|
84
87
|
registerRepairCommand(program);
|
|
88
|
+
registerResultsCommand(program);
|
|
85
89
|
registerIngestCommand(program);
|
|
86
90
|
registerEvalCommand(program);
|
|
87
91
|
registerLoginCommand(program);
|
|
@@ -22,9 +22,17 @@ export function loadApiCatalog(
|
|
|
22
22
|
kind: 'screen' | 'flow' | 'api',
|
|
23
23
|
screen: string,
|
|
24
24
|
): Record<string, ApiCatalogEntry> {
|
|
25
|
-
|
|
25
|
+
// Every kind can carry `@api:` preconditions, and the runtime resolves their
|
|
26
|
+
// endpoints from the UNIT's own catalog first (qa/screens|flows/<u>/api/apis.yaml)
|
|
27
|
+
// then the shared one — this loader mirrors that scheme so the deliverable can
|
|
28
|
+
// name the endpoint a screen/flow scenario actually calls.
|
|
29
|
+
const unitDir = kind === 'api'
|
|
30
|
+
? path.join(cwd, 'qa', 'api', screen)
|
|
31
|
+
: kind === 'flow'
|
|
32
|
+
? path.join(cwd, 'qa', 'flows', screen)
|
|
33
|
+
: path.join(cwd, 'qa', 'screens', screen);
|
|
26
34
|
const shared = readApiCatalogFile(path.join(cwd, 'qa', 'api', 'apis.yaml'));
|
|
27
|
-
const unit = readApiCatalogFile(path.join(
|
|
35
|
+
const unit = readApiCatalogFile(path.join(unitDir, 'api', 'apis.yaml'));
|
|
28
36
|
return { ...shared, ...unit };
|
|
29
37
|
}
|
|
30
38
|
|
|
@@ -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
|