@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
|
@@ -46,7 +46,12 @@ Hard-fail gate. Any ✗ is a syntax error — must be fixed; do not score furthe
|
|
|
46
46
|
- [ ] `Given` → only `is on` · `When` → action (click/fill/select/press/clear/check/uncheck/hover/wait) · `Then` → only `see` · `And` inherits the preceding keyword
|
|
47
47
|
- [ ] NO `Given User see…` (→ `Then`, or entry assertion `Given User is on [X] page`)
|
|
48
48
|
- [ ] NO `When … And User is on [X] dialog` (→ `And User see` or a separate `Given`)
|
|
49
|
+
- [ ] `dismiss [X]` is SETUP, never proof — it is best-effort and never fails, so a scenario that
|
|
50
|
+
claims an overlay closed must assert it: `Then User see [X] is hidden` (mobile: tap the
|
|
51
|
+
close/scrim node explicitly, then assert)
|
|
52
|
+
- [ ] `is on` takes ONLY `[X] page` (`[X] screen` on mobile) / `[X] dialog` — `is on [X] step|section|card` ✗ (it compiles to a real navigation, or to a URL assertion that cannot fail). A state inside the page → `Then User see [X] section`.
|
|
49
53
|
- [ ] NO literal-URL navigation (`navigate to "/x"`) → `User is on [X] page` + declare in `selectors.yaml`
|
|
54
|
+
- [ ] NO `{{var}}` inside a `[Target]` (`[{{confirm_value}}]` ✗) — a target is a selector key matched literally. For text that varies (locale, `@cases` column) keep the target static and move the variable into the selector entry's `name:`/`value:`, or pick by text with `with {{var}}`. Only a browser-storage key may embed one.
|
|
50
55
|
|
|
51
56
|
### A2. Action → Element type
|
|
52
57
|
|
|
@@ -67,6 +72,16 @@ Hard-fail gate. Any ✗ is a syntax error — must be fixed; do not score furthe
|
|
|
67
72
|
### A4. Assertion (8 patterns → determines the Playwright assertion)
|
|
68
73
|
|
|
69
74
|
- [ ] Visibility `see [T] type` → NO redundant `is visible`; hidden uses `is hidden`
|
|
75
|
+
- [ ] **No state clause on `[T] page`** — `page is hidden`/`is empty`/… are not step forms. `page`
|
|
76
|
+
asserts a URL, so the clause is dropped and the assertion inverts (`SG-E021`). Assert the
|
|
77
|
+
expected page, or a marker element
|
|
78
|
+
- [ ] **The state is one the step form can actually assert** — a `wait for` honours only
|
|
79
|
+
`hidden`/`visible`/`disabled`/`enabled`; `row in [T] table` / `column in [T] table` honour
|
|
80
|
+
only `is hidden`. Anything else is dropped (`SG-E021`) — move the state onto the element
|
|
81
|
+
- [ ] **No invented step forms** — every step matches a pattern in `sungen-gherkin-syntax`. An
|
|
82
|
+
unmatched step compiles to a runtime `throw` (`SG-W001`), never a silent pass; park a
|
|
83
|
+
genuinely unautomatable check as `@manual`
|
|
84
|
+
- [ ] A spec `Format:` line → `matches "<format>"`, NOT `with {{v}}` (an exact value pins data that changes daily; `contains` proves nothing)
|
|
70
85
|
- [ ] Correct pattern: text(`message/header/label with {{v}}`) · partial(`text contains {{v}}`) · input(`field/dropdown with {{v}}`) · state(`is …`) · attribute(`image/link with {{v}}`) · count(`row with {{count}}`) · page(`[T] page`)
|
|
71
86
|
- [ ] Table: `[Col] column in [Table] table`, `[Ref] row in [Table] table with {{v}}`, `table with {{count}}`/`is empty`, `table match data:`; row scope used correctly
|
|
72
87
|
|
|
@@ -23,3 +23,24 @@ JAVA_HOME=/opt/homebrew/opt/openjdk@21
|
|
|
23
23
|
# IOS_UDID=<device-udid> # REQUIRED — physical device (`xcrun xctrace list devices`), not a sim
|
|
24
24
|
# IOS_BUNDLE_ID=<ios bundle id> # REQUIRED — the iOS bundle (⚠ NOT the Android package)
|
|
25
25
|
# IOS_TEAM_ID=<Apple Team ID> # REQUIRED — 10-char id (Xcode → Settings → Accounts)
|
|
26
|
+
|
|
27
|
+
# ════════════ State reset between scenarios ════════════
|
|
28
|
+
# Raises the reset floor for a WHOLE run without regenerating any spec — CI can run stricter than
|
|
29
|
+
# local dev. It never LOWERS a tier a feature declared with @reset:. Tiers, weakest → strongest:
|
|
30
|
+
# none nothing reset (state carries over by design) 0s
|
|
31
|
+
# soft terminate + relaunch — THE DEFAULT ~1–3s
|
|
32
|
+
# data + wipe local DB / cache / prefs / keychain ~2–5s (iOS: reinstalls, needs IOS_APP)
|
|
33
|
+
# full + remove and reinstall (permissions too) ~5–30s (needs APP_APK / IOS_APP)
|
|
34
|
+
# ⚠ data and full WIPE APP DATA on the target device — emulator / dedicated test device only.
|
|
35
|
+
# SUNGEN_RESET_TIER=data
|
|
36
|
+
|
|
37
|
+
# ════════════ Appium server security (opt-in) ════════════
|
|
38
|
+
# Allow `mobile: shell` (raw adb passthrough) for device-level oracles (e.g. pulling an app's
|
|
39
|
+
# SQLite DB via run-as). Starts the server with --allow-insecure=uiautomator2:adb_shell.
|
|
40
|
+
# SECURITY: gives the session shell access — enable only on trusted local/dev machines,
|
|
41
|
+
# never on shared CI runners you don't control.
|
|
42
|
+
# APPIUM_ALLOW_ADB_SHELL=1
|
|
43
|
+
|
|
44
|
+
# Hybrid WebView: auto-download the chromedriver matching the device's WebView (needed by
|
|
45
|
+
# `switch to [X] frame` on hybrid screens). Downloads a binary at session start — opt-in.
|
|
46
|
+
# APPIUM_CHROMEDRIVER_AUTODOWNLOAD=1
|
|
@@ -58,6 +58,124 @@ function substitute(text: string, params: Record<string, any>): string {
|
|
|
58
58
|
function substituteRaw(text: string, params: Record<string, any>): string {
|
|
59
59
|
return text.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, p) => String(params[p] ?? ''));
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/** The five predefined XML entities — a bound value containing a bare `&` or `<` would
|
|
63
|
+
* otherwise re-create exactly the invalid_xml rejection this encoding exists to avoid. */
|
|
64
|
+
function escapeXml(value: string): string {
|
|
65
|
+
return value
|
|
66
|
+
.replace(/&/g, '&')
|
|
67
|
+
.replace(/</g, '<')
|
|
68
|
+
.replace(/>/g, '>')
|
|
69
|
+
.replace(/"/g, '"')
|
|
70
|
+
.replace(/'/g, ''');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Bind `:param` tokens into an XML string template by WHITELIST of the passed param
|
|
75
|
+
* names — a generic `:word` scan would mangle namespace prefixes (soapenv:Envelope).
|
|
76
|
+
* Longest name first + a word-boundary lookahead so `:id` can never eat the prefix
|
|
77
|
+
* of `:id_2` or the head of `:idx`. Values are XML-escaped by default; a token the
|
|
78
|
+
* template wraps as `<![CDATA[:param]]>` binds raw inside its CDATA section instead.
|
|
79
|
+
* Replacer FUNCTIONS everywhere — a value containing `$&`-style sequences must never
|
|
80
|
+
* be interpreted as a replacement pattern.
|
|
81
|
+
*/
|
|
82
|
+
function bindXmlParams(template: string, params: Record<string, any>): string {
|
|
83
|
+
let out = template;
|
|
84
|
+
for (const name of Object.keys(params).sort((a, b) => b.length - a.length)) {
|
|
85
|
+
const value = String(params[name] ?? '');
|
|
86
|
+
// A value containing "]]>" would terminate the CDATA section early and ship
|
|
87
|
+
// malformed XML — split it across two sections so the document stays valid.
|
|
88
|
+
const cdataSafe = value.split(']]>').join(']]]]><![CDATA[>');
|
|
89
|
+
out = out.replace(new RegExp(`<!\\[CDATA\\[:${name}\\]\\]>`, 'g'), () => `<![CDATA[${cdataSafe}]]>`);
|
|
90
|
+
out = out.replace(new RegExp(`:${name}(?![A-Za-z0-9_])`, 'g'), () => escapeXml(value));
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Decode an XML-ish response body: Content-Type charset, else the XML declaration's
|
|
96
|
+
* encoding (read from an ASCII-compatible prefix), else UTF-8. Unknown charsets fall
|
|
97
|
+
* back to UTF-8 rather than failing the whole request. */
|
|
98
|
+
function decodeXmlAwareText(bytes: Buffer, contentType: string): string {
|
|
99
|
+
// A UTF-16 BOM means the latin1 declaration scan below would see NUL-laden
|
|
100
|
+
// garbage that still "parses" \u2014 decode by the BOM first.
|
|
101
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) return new TextDecoder('utf-16le').decode(bytes);
|
|
102
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) return new TextDecoder('utf-16be').decode(bytes);
|
|
103
|
+
const head = bytes.subarray(0, 200).toString('latin1');
|
|
104
|
+
const declared = (head.match(/^\uFEFF?\s*<\?xml[^>]*\bencoding=["']([^"']+)["']/i) || [])[1];
|
|
105
|
+
// Candidates in precedence order; an unsupported CT charset falls back to the
|
|
106
|
+
// XML declaration's encoding rather than straight to a mojibake UTF-8 decode.
|
|
107
|
+
const candidates = [(contentType.match(/charset=["']?([^;"'\s]+)/i) || [])[1], declared, 'utf-8'];
|
|
108
|
+
for (const charset of candidates) {
|
|
109
|
+
if (!charset) continue;
|
|
110
|
+
try { return new TextDecoder(charset).decode(bytes); } catch { /* next candidate */ }
|
|
111
|
+
}
|
|
112
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parse an XML response body to a plain object, or null on a real parse failure (the
|
|
117
|
+
* caller then keeps the raw string, matching the non-JSON behavior).
|
|
118
|
+
*
|
|
119
|
+
* Locked parser posture:
|
|
120
|
+
* - processEntities/htmlEntities OFF — fast-xml-parser's entity processing (ON by
|
|
121
|
+
* default, including DOCTYPE entities) is exactly the XXE-shaped surface a QA tool
|
|
122
|
+
* must not expose to a live server's response. The five predefined entities are
|
|
123
|
+
* decoded AFTERWARDS by our own pass.
|
|
124
|
+
* - parseTagValue/parseAttributeValue OFF — `<error_code>01000001</error_code>` must
|
|
125
|
+
* stay a string; numeric coercion destroys leading zeros.
|
|
126
|
+
* - attributes kept with the `@_` prefix (`{{r.body.tag.@_id}}`).
|
|
127
|
+
* - repeated elements: XML cannot distinguish "one item" from "a list of one" — the
|
|
128
|
+
* catalog declares `arrayPaths:` (dot jpaths INCLUDING the root element) and those
|
|
129
|
+
* paths are ALWAYS arrays, so `[0]` access works for 1 and N occurrences alike
|
|
130
|
+
* (an EMPTY parent has no child key at all — no array to index).
|
|
131
|
+
*/
|
|
132
|
+
function parseXmlBody(text: string, label: string, arrayPaths?: string[]): any | null {
|
|
133
|
+
let XMLParser: any;
|
|
134
|
+
try {
|
|
135
|
+
// Lazy require: generated projects created before this capability existed don't
|
|
136
|
+
// have the dep; they must fail with the install command, not MODULE_NOT_FOUND.
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
138
|
+
({ XMLParser } = require('fast-xml-parser'));
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error(`API Driver: ${label} — the response is XML but "fast-xml-parser" is not installed in this project. Run: npm install --save-dev fast-xml-parser`);
|
|
141
|
+
}
|
|
142
|
+
const arraySet = new Set(arrayPaths || []);
|
|
143
|
+
const parser = new XMLParser({
|
|
144
|
+
ignoreAttributes: false,
|
|
145
|
+
attributeNamePrefix: '@_',
|
|
146
|
+
processEntities: false,
|
|
147
|
+
htmlEntities: false,
|
|
148
|
+
parseTagValue: false,
|
|
149
|
+
parseAttributeValue: false,
|
|
150
|
+
trimValues: true,
|
|
151
|
+
isArray: (_name: string, jpath: string) => arraySet.has(jpath),
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
return decodeXmlEntitiesDeep(parser.parse(text));
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Decode the five predefined XML entities in every string leaf (`&` last, so a
|
|
161
|
+
* literal `&lt;` decodes to `<` and not further). */
|
|
162
|
+
function decodeXmlEntitiesDeep(value: any): any {
|
|
163
|
+
if (typeof value === 'string') {
|
|
164
|
+
return value
|
|
165
|
+
.replace(/</g, '<')
|
|
166
|
+
.replace(/>/g, '>')
|
|
167
|
+
.replace(/"/g, '"')
|
|
168
|
+
.replace(/'/g, "'")
|
|
169
|
+
.replace(/&/g, '&');
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(value)) return value.map(decodeXmlEntitiesDeep);
|
|
172
|
+
if (value !== null && typeof value === 'object') {
|
|
173
|
+
const out: Record<string, any> = {};
|
|
174
|
+
for (const [k, v] of Object.entries(value)) out[k] = decodeXmlEntitiesDeep(v);
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
61
179
|
/** Delete any case-insensitive variant of a header (HTTP header names are case-insensitive, but a
|
|
62
180
|
* plain object keys on exact case — so a datasource `Content-Type` and our `content-type` would
|
|
63
181
|
* both be sent). Call before setting, or to let fetch own the header (multipart boundary). */
|
|
@@ -94,7 +212,11 @@ function loadStorageStateCookieHeader(storageStatePath: string, url: string): st
|
|
|
94
212
|
const targetIsSecure = target.protocol === 'https:';
|
|
95
213
|
const matches = cookies.filter((c) => {
|
|
96
214
|
const domain = (c.domain || '').replace(/^\./, '');
|
|
97
|
-
|
|
215
|
+
// A cookie with NO domain must match nothing, not everything — Playwright
|
|
216
|
+
// always records a domain, so a domainless entry only comes from a hand-
|
|
217
|
+
// edited state file, and treating it as a wildcard would attach the session
|
|
218
|
+
// cookie to any origin a response-bound absolute URL points at.
|
|
219
|
+
const domainOk = !!domain && (target.hostname === domain || target.hostname.endsWith(`.${domain}`));
|
|
98
220
|
const pathOk = target.pathname.startsWith(c.path || '/');
|
|
99
221
|
const notExpired = c.expires === undefined || c.expires < 0 || c.expires > nowSec;
|
|
100
222
|
// A Secure cookie must never leave over plaintext. Playwright's own cookie jar enforces this,
|
|
@@ -201,22 +323,72 @@ class ApiClient {
|
|
|
201
323
|
*/
|
|
202
324
|
async call(
|
|
203
325
|
label: string,
|
|
204
|
-
req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; files?: Record<string, FileSpec | FileSpec[]>; bodyFile?: FileSpec; headers?: Record<string, string>; timeout?: number; datasource?: string },
|
|
326
|
+
req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart' | 'xml'; files?: Record<string, FileSpec | FileSpec[]>; bodyFile?: FileSpec; headers?: Record<string, string>; timeout?: number; datasource?: string; arrayPaths?: string[] },
|
|
205
327
|
params: Record<string, any> = {},
|
|
206
328
|
opts: { storageState?: string } = {},
|
|
207
329
|
): Promise<{ status: number; ok: boolean; body: any; headers: Record<string, string> }> {
|
|
208
330
|
const { conf } = this.cfg(req.datasource);
|
|
209
331
|
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
210
332
|
if (!base) throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
211
|
-
const urlPath = substitute(req.path, params); // path params (:id) bind at runtime
|
|
212
333
|
|
|
213
|
-
|
|
334
|
+
// Whole-token path (`path: ":url"`): bind RAW and use an absolute http(s)
|
|
335
|
+
// value directly — an export/download link returned by a prior response
|
|
336
|
+
// (often on another origin, e.g. blob storage). substitute() would percent-
|
|
337
|
+
// encode the whole URL into `https%3A%2F%2F…` and joinApiUrl would glue it
|
|
338
|
+
// onto base_url. Raw binding also keeps presigned query escapes (%2F)
|
|
339
|
+
// untouched; new URL() is used for VALIDATION only (a response value can be
|
|
340
|
+
// anything — file:/data: schemes are refused, never fetched).
|
|
341
|
+
let fullUrl: string;
|
|
342
|
+
let absolute = false;
|
|
343
|
+
const wholeToken = req.path.match(/^:([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
344
|
+
if (wholeToken) {
|
|
345
|
+
// The bound value MUST be an absolute http(s) URL. Anything else — empty
|
|
346
|
+
// string (a broken export returning {"url":""}), a relative path, or a
|
|
347
|
+
// data:/mailto:/file: form — is refused loudly: falling through to the
|
|
348
|
+
// legacy base_url join would silently GET the API root and let the
|
|
349
|
+
// scenario false-pass on exactly the failure it exists to catch. (A
|
|
350
|
+
// whole-token path was never valid before this feature, so there is no
|
|
351
|
+
// legacy behavior to preserve.)
|
|
352
|
+
const wholeVal = String(params[wholeToken[1]] ?? '');
|
|
353
|
+
let parsed: URL | null = null;
|
|
354
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(wholeVal)) {
|
|
355
|
+
try { parsed = new URL(wholeVal); } catch { parsed = null; }
|
|
356
|
+
}
|
|
357
|
+
if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {
|
|
358
|
+
throw new Error(`API Driver: ${label} — :${wholeToken[1]} must be an absolute http(s) URL (the value is bound from a live response); got: ${JSON.stringify(wholeVal)}`);
|
|
359
|
+
}
|
|
360
|
+
if (parsed.username || parsed.password) {
|
|
361
|
+
throw new Error(`API Driver: ${label} — :${wholeToken[1]} must not carry credentials (user:pass@host): fetch refuses such URLs.`);
|
|
362
|
+
}
|
|
363
|
+
// Raw string, not parsed.toString(): URL normalization could rewrite the
|
|
364
|
+
// presigned query escapes (%2F) the storage host signed.
|
|
365
|
+
fullUrl = wholeVal;
|
|
366
|
+
absolute = true;
|
|
367
|
+
} else if (/^https?:\/\//i.test(req.path)) {
|
|
368
|
+
// Literal absolute catalog path — its :params (if any) bind with the normal encoding.
|
|
369
|
+
fullUrl = substitute(req.path, params);
|
|
370
|
+
absolute = true;
|
|
371
|
+
} else {
|
|
372
|
+
fullUrl = joinApiUrl(base, substitute(req.path, params)); // path params (:id) bind at runtime
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Cross-origin absolute URL: the datasource's own headers must NOT travel to
|
|
376
|
+
// another origin — auth tokens, cookies and tenant keys live in conf.headers
|
|
377
|
+
// with no metadata separating them from benign defaults, so ALL of them stay
|
|
378
|
+
// home; only headers declared on the entry itself are sent. An unparseable
|
|
379
|
+
// base counts as a different origin (fail closed).
|
|
380
|
+
const sameOrigin = !absolute || (() => {
|
|
381
|
+
try { return new URL(fullUrl).origin === new URL(base).origin; } catch { return false; }
|
|
382
|
+
})();
|
|
383
|
+
const headers: Record<string, string> = sameOrigin ? { ...(conf.headers || {}) } : {};
|
|
214
384
|
// catalog headers; :param tokens bind at runtime — raw (no URL-encoding, unlike the path)
|
|
215
385
|
for (const [k, v] of Object.entries(req.headers || {}))
|
|
216
386
|
headers[k] = substituteRaw(String(v), params);
|
|
217
387
|
// Body: substitute `:param` into the body template (object values), then encode per `encoding`.
|
|
388
|
+
// An XML body is a raw STRING template and binds by param-name WHITELIST below —
|
|
389
|
+
// the generic whole-value substitution here must not touch it.
|
|
218
390
|
let body: any;
|
|
219
|
-
if (req.body !== undefined && req.body !== null) {
|
|
391
|
+
if (req.body !== undefined && req.body !== null && req.encoding !== 'xml') {
|
|
220
392
|
body = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
221
393
|
}
|
|
222
394
|
// Map the wire format to a fetch request body (#345): json → JSON string (application/json,
|
|
@@ -244,6 +416,17 @@ class ApiClient {
|
|
|
244
416
|
// drop any datasource default Content-Type so it doesn't conflict with / duplicate it.
|
|
245
417
|
deleteHeader(headers, 'content-type');
|
|
246
418
|
fetchBody = buildMultipart(req.files, body, params, label);
|
|
419
|
+
} else if (req.encoding === 'xml' && req.body !== undefined && req.body !== null) {
|
|
420
|
+
// XML body: the catalog carries a raw string template (any shape — attributes,
|
|
421
|
+
// namespaces, CDATA, nesting). :params bind by WHITELIST of the passed param
|
|
422
|
+
// names, never by a generic `:word` scan — that scan would mangle namespace
|
|
423
|
+
// prefixes (soapenv:Envelope, xsi:type) into bound values. A BODYLESS xml
|
|
424
|
+
// entry is fine — a GET whose xml RESPONSE needs arrayPaths carries no body.
|
|
425
|
+
if (typeof req.body !== 'string') {
|
|
426
|
+
throw new Error(`API Driver: ${label} — encoding: xml requires body: to be a string template (a YAML block scalar holding the raw XML).`);
|
|
427
|
+
}
|
|
428
|
+
fetchBody = bindXmlParams(req.body, params);
|
|
429
|
+
if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/xml';
|
|
247
430
|
} else if (body !== undefined) {
|
|
248
431
|
const enc = req.encoding ?? 'json';
|
|
249
432
|
if (enc === 'form') {
|
|
@@ -266,9 +449,6 @@ class ApiClient {
|
|
|
266
449
|
throw new Error(`API Driver: ${label} — a ${req.method.toUpperCase()} request cannot carry a body. Remove body:/files:/bodyFile: from the catalog entry, or change its method to POST/PUT/PATCH.`);
|
|
267
450
|
}
|
|
268
451
|
|
|
269
|
-
// Full URL (not a baseURL-relative path) so a base path component like /api/v3 is preserved.
|
|
270
|
-
const fullUrl = joinApiUrl(base, urlPath);
|
|
271
|
-
|
|
272
452
|
// @hybrid: `storageState` is the @auth role's saved session (Playwright's `context.storageState()`
|
|
273
453
|
// JSON) — reuse its cookies (scoped to `fullUrl`'s domain/path) so the request shares the
|
|
274
454
|
// browser's authenticated session without a browser/Playwright context.
|
|
@@ -292,9 +472,32 @@ class ApiClient {
|
|
|
292
472
|
...(fetchBody !== undefined ? { body: fetchBody } : {}),
|
|
293
473
|
signal: controller.signal,
|
|
294
474
|
});
|
|
295
|
-
|
|
475
|
+
// Read bytes, not res.text(): the fetch spec hard-decodes text() as UTF-8, so a
|
|
476
|
+
// Shift_JIS/ISO-8859-1 XML response (common from OTA backends) would mojibake
|
|
477
|
+
// silently. XML-ish responses decode by Content-Type charset, then the XML
|
|
478
|
+
// declaration's encoding, then UTF-8; everything else keeps the UTF-8 decode
|
|
479
|
+
// res.text() always performed.
|
|
480
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
481
|
+
const contentType = res.headers.get('content-type') || '';
|
|
482
|
+
// XML detection: an explicit xml Content-Type (text/xml, application/xml,
|
|
483
|
+
// application/soap+xml — never text/html), else — only when the CT is absent or
|
|
484
|
+
// octet-stream — a sniff REQUIRING the `<?xml` declaration. Never sniff on a bare
|
|
485
|
+
// `<`: the parser does not validate, so an HTML 502 page would come back as a
|
|
486
|
+
// junk object instead of the raw string an author can at least read.
|
|
487
|
+
const ctIsXml = /xml/i.test(contentType) && !/html/i.test(contentType);
|
|
488
|
+
const maySniff = !contentType || /octet-stream/i.test(contentType);
|
|
489
|
+
// Non-XML branch uses TextDecoder, NOT Buffer.toString: the fetch spec's
|
|
490
|
+
// "UTF-8 decode" (what res.text() did) STRIPS a leading BOM, and a BOM'd
|
|
491
|
+
// JSON response must keep parsing as an object exactly as before.
|
|
492
|
+
const text = ctIsXml || maySniff ? decodeXmlAwareText(bytes, contentType) : new TextDecoder('utf-8').decode(bytes);
|
|
493
|
+
const isXml = (ctIsXml || (maySniff && /^\uFEFF?\s*<\?xml/.test(text))) && text.length > 0;
|
|
296
494
|
let parsed: any = text;
|
|
297
|
-
|
|
495
|
+
if (isXml) {
|
|
496
|
+
const xml = parseXmlBody(text, label, req.arrayPaths);
|
|
497
|
+
parsed = xml !== null ? xml : text; // real parse failure → keep the raw string
|
|
498
|
+
} else {
|
|
499
|
+
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
500
|
+
}
|
|
298
501
|
return { status: res.status, ok: res.ok, body: parsed, headers: Object.fromEntries(res.headers.entries()) };
|
|
299
502
|
} finally {
|
|
300
503
|
clearTimeout(timer);
|
|
@@ -310,7 +513,7 @@ class ApiClient {
|
|
|
310
513
|
*/
|
|
311
514
|
async callN(
|
|
312
515
|
label: string,
|
|
313
|
-
req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart'; files?: Record<string, FileSpec | FileSpec[]>; bodyFile?: FileSpec; headers?: Record<string, string>; timeout?: number; datasource?: string },
|
|
516
|
+
req: { method: string; path: string; body?: unknown; encoding?: 'json' | 'form' | 'multipart' | 'xml'; files?: Record<string, FileSpec | FileSpec[]>; bodyFile?: FileSpec; headers?: Record<string, string>; timeout?: number; datasource?: string; arrayPaths?: string[] },
|
|
314
517
|
params: Record<string, any> = {},
|
|
315
518
|
n = 1,
|
|
316
519
|
opts: { storageState?: string } = {},
|
|
@@ -27,7 +27,7 @@ const ident = (s: string): string => {
|
|
|
27
27
|
// SQL engines: verified via the direct guarded-SELECT path and needing the `$n`→`?` placeholder
|
|
28
28
|
// rewrite. This literal is the single source of truth for that rewrite condition and is quoted, by
|
|
29
29
|
// name, in the sungen-gherkin-syntax skill's "direct SQL verification" note — keep them in sync.
|
|
30
|
-
const SUPPORTED_ENGINES = ['postgres', 'sqlite', 'mysql'] as const;
|
|
30
|
+
const SUPPORTED_ENGINES = ['postgres', 'sqlite', 'device-sqlite', 'mysql'] as const;
|
|
31
31
|
// NoSQL engines: implement the Engine contract natively (no SQL rewrite). Tracked separately so the
|
|
32
32
|
// SQL-rewrite guard and its skill-parity test stay pinned to the three SQL engines above.
|
|
33
33
|
const NOSQL_ENGINES = ['cosmos', 'mongodb', 'dynamodb'] as const;
|
|
@@ -43,9 +43,15 @@ interface SshConfig {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
interface DataSourceConfig {
|
|
46
|
-
engine: 'postgres' | 'mysql' | 'sqlite' | 'cosmos' | 'mongodb' | 'dynamodb';
|
|
46
|
+
engine: 'postgres' | 'mysql' | 'sqlite' | 'device-sqlite' | 'cosmos' | 'mongodb' | 'dynamodb';
|
|
47
47
|
url: string;
|
|
48
48
|
readonly?: boolean;
|
|
49
|
+
// device-sqlite (mobile): an ON-DEVICE app database, pulled FRESH per query through the active
|
|
50
|
+
// Appium session (pullFile's `@<package>/<path>` app-container syntax — run-as under the hood,
|
|
51
|
+
// so it needs a debuggable build or an emulator). `package` = the app id, `db_path` = the
|
|
52
|
+
// path inside the app's data dir (e.g. databases/app.db). No `url`. Same SQLite dialect.
|
|
53
|
+
package?: string;
|
|
54
|
+
db_path?: string;
|
|
49
55
|
statement_timeout_ms?: number;
|
|
50
56
|
max_rows?: number;
|
|
51
57
|
// Fallback path: tunnel the DB SOCKET through an SSH bastion. DB-only — the browser/E2E
|
|
@@ -160,7 +166,7 @@ function loadConfig(): Record<string, DataSourceConfig> {
|
|
|
160
166
|
* reorder is essential there too (an author-written PartiQL `WHERE sk = $2 AND pk = $1`).
|
|
161
167
|
*/
|
|
162
168
|
export function rewritePlaceholders(engine: string, sql: string, params: any[]): { sql: string; params: any[] } {
|
|
163
|
-
if (engine !== 'sqlite' && engine !== 'mysql' && engine !== 'dynamodb') return { sql, params };
|
|
169
|
+
if (engine !== 'sqlite' && engine !== 'device-sqlite' && engine !== 'mysql' && engine !== 'dynamodb') return { sql, params };
|
|
164
170
|
let out = '';
|
|
165
171
|
let inStr = false;
|
|
166
172
|
const newParams: any[] = [];
|
|
@@ -517,10 +523,11 @@ class DataSource {
|
|
|
517
523
|
const { key, conf } = this.cfg(name);
|
|
518
524
|
if (this.engines.has(key)) return { engine: this.engines.get(key)!, conf };
|
|
519
525
|
let engine: Engine;
|
|
520
|
-
if (conf.engine === 'postgres' || conf.engine === 'mysql' || conf.engine === 'sqlite') {
|
|
521
|
-
// url is required for the SQL engines (
|
|
522
|
-
//
|
|
523
|
-
|
|
526
|
+
if (conf.engine === 'postgres' || conf.engine === 'mysql' || conf.engine === 'sqlite' || conf.engine === 'device-sqlite') {
|
|
527
|
+
// url is required for the HOST-based SQL engines (or a sqlite file path). device-sqlite has
|
|
528
|
+
// no url — the file comes off the device (package + db_path, validated in its exec builder);
|
|
529
|
+
// NoSQL engines without a URL (e.g. DynamoDB region+creds) also supply their own config.
|
|
530
|
+
if (!conf.url && conf.engine !== 'device-sqlite') throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
|
|
524
531
|
engine = new SqlEngine(conf.engine, await this.sqlExec(key, conf));
|
|
525
532
|
} else if (conf.engine === 'cosmos') {
|
|
526
533
|
engine = this.cosmosEngine(key, conf);
|
|
@@ -586,6 +593,62 @@ class DataSource {
|
|
|
586
593
|
}
|
|
587
594
|
};
|
|
588
595
|
}
|
|
596
|
+
// device-sqlite — an ON-DEVICE app database. Every query pulls a FRESH copy through the
|
|
597
|
+
// active Appium session so the oracle always reads CURRENT device state (persistence checks
|
|
598
|
+
// assert AFTER actions). pullFile's `@<package>/<path>` app-container syntax uses run-as
|
|
599
|
+
// under the hood → needs a debuggable build or an emulator. SQLite WAL sidecars (-wal/-shm)
|
|
600
|
+
// are pulled best-effort alongside: Room defaults to WAL, where recent commits live in the
|
|
601
|
+
// -wal file — without it the main file silently misses the newest writes.
|
|
602
|
+
if (conf.engine === 'device-sqlite') {
|
|
603
|
+
if (!conf.package || !conf.db_path) {
|
|
604
|
+
throw new Error(`Data Driver: datasource "${key}" (device-sqlite) needs package: and db_path: (path inside the app data dir, e.g. databases/app.db).`);
|
|
605
|
+
}
|
|
606
|
+
const pkg = conf.package;
|
|
607
|
+
const rel = conf.db_path.replace(/^\/+/, '');
|
|
608
|
+
return async (sql, params) => {
|
|
609
|
+
const wd = (globalThis as any).driver;
|
|
610
|
+
if (!wd || typeof wd.pullFile !== 'function') {
|
|
611
|
+
throw new Error(`Data Driver: datasource "${key}" (device-sqlite) needs an active Appium session — device @query steps only run inside mobile specs.`);
|
|
612
|
+
}
|
|
613
|
+
const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path');
|
|
614
|
+
const base = path.join(os.tmpdir(), `sungen-devdb-${process.pid}-${Date.now()}`);
|
|
615
|
+
const files: string[] = [];
|
|
616
|
+
const pull = async (suffix: string, required: boolean): Promise<void> => {
|
|
617
|
+
let b64: string | null = null;
|
|
618
|
+
let pullErr: unknown;
|
|
619
|
+
try {
|
|
620
|
+
b64 = await wd.pullFile(`@${pkg}/${rel}${suffix}`);
|
|
621
|
+
} catch (e) {
|
|
622
|
+
pullErr = e;
|
|
623
|
+
// pullFile's container route does `run-as cp → /data/local/tmp`, which newer Android
|
|
624
|
+
// blocks for the app UID. Fallback: stream base64 over run-as via `mobile: shell`
|
|
625
|
+
// (opt-in — APPIUM_ALLOW_ADB_SHELL=1, see the wdio.conf template).
|
|
626
|
+
try {
|
|
627
|
+
const out = await wd.execute('mobile: shell', { command: 'run-as', args: [pkg, 'base64', `${rel}${suffix}`] });
|
|
628
|
+
b64 = String(out).replace(/\s+/g, '');
|
|
629
|
+
} catch (e2) {
|
|
630
|
+
if (required) {
|
|
631
|
+
throw new Error(`Data Driver: could not pull @${pkg}/${rel} from the device. pullFile: ${pullErr} · shell fallback: ${e2} — needs a DEBUGGABLE build; if pullFile's container route fails on this Android version, set APPIUM_ALLOW_ADB_SHELL=1 (.env.appium).`);
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (b64 == null) return;
|
|
637
|
+
fs.writeFileSync(base + suffix, Buffer.from(b64, 'base64'));
|
|
638
|
+
files.push(base + suffix);
|
|
639
|
+
};
|
|
640
|
+
await pull('', true);
|
|
641
|
+
await pull('-wal', false);
|
|
642
|
+
await pull('-shm', false);
|
|
643
|
+
try {
|
|
644
|
+
const Database = require('better-sqlite3');
|
|
645
|
+
const db = new Database(base, { readonly: false }); // WAL recovery needs write access to the TEMP copy
|
|
646
|
+
try { return db.prepare(sql).all(...params); } finally { db.close(); }
|
|
647
|
+
} finally {
|
|
648
|
+
for (const f of files) { try { fs.unlinkSync(f); } catch { /* best-effort */ } }
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
589
652
|
// sqlite
|
|
590
653
|
if (conf.ssh) console.warn(`Data Driver: datasource "${key}" sets ssh: but engine is sqlite (file-based) — ssh ignored.`);
|
|
591
654
|
const Database = require('better-sqlite3');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Sungen regex-escape helper — auto-generated into specs/regex-escape.ts. DO NOT EDIT.
|
|
4
|
+
*
|
|
5
|
+
* Test-data values are LITERAL TEXT. When a generated assertion needs an anchored
|
|
6
|
+
* match (`filter({ hasText: /^…$/ })`), the runtime value is interpolated into a
|
|
7
|
+
* `new RegExp(...)` — so a value containing `( [ $ . * +` would either throw a
|
|
8
|
+
* SyntaxError or silently match the wrong element unless it is escaped HERE, at
|
|
9
|
+
* runtime, where the real value is finally known. Template-time escaping cannot do
|
|
10
|
+
* this: at generate time the value is still a placeholder marker.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Escape a runtime string so RegExp treats every character literally. */
|
|
14
|
+
export function escapeRegExp(text: string): string {
|
|
15
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
}
|
|
@@ -50,16 +50,42 @@ export class TestDataLoader {
|
|
|
50
50
|
if (value === undefined || value === null) {
|
|
51
51
|
throw new Error(`Test data key not found: ${key}`);
|
|
52
52
|
}
|
|
53
|
-
// A projected array (e.g. `{{q.rows[*].name}}`)
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
// A projected array (e.g. `{{q.rows[*].name}}`) and a PLAIN object (an API response
|
|
54
|
+
// body) serialize as JSON text — String([...]) flattens to a lossy comma-join and
|
|
55
|
+
// String({}) is "[object Object]", both un-assertable. Embedded `{{ref}}`s interpolate
|
|
56
|
+
// INSIDE the value before stringify, so a ref carrying quotes/backslashes can never
|
|
57
|
+
// corrupt the serialized JSON (the old order substituted raw text into JSON text).
|
|
58
|
+
// Only PLAIN objects take this path: Date/Buffer/ObjectId values bound by @query from
|
|
59
|
+
// a real driver keep their String()/toString() output — JSON.stringify would wrap a
|
|
60
|
+
// Date in quotes and flip every assertion that already compares its string form.
|
|
61
|
+
const result = isJsonContainer(value)
|
|
62
|
+
? JSON.stringify(this.interpolateDeep(value))
|
|
58
63
|
: this.interpolate(String(value));
|
|
59
64
|
assertNoUnresolvedEnvRef(key, result);
|
|
60
65
|
return result;
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Interpolate `{{ref}}`s in every string leaf AND key of a JSON container,
|
|
70
|
+
* recursively (matching the old whole-JSON-text pass). Non-plain nested
|
|
71
|
+
* objects are passed through untouched — JSON.stringify serializes them via
|
|
72
|
+
* toJSON. A truly cyclic object throws a RangeError; every current bind
|
|
73
|
+
* source (JSON.parse output, DB rows, YAML after load-time resolution) is
|
|
74
|
+
* acyclic. Built with Object.fromEntries, which DEFINES data properties — a
|
|
75
|
+
* body field literally named "__proto__" survives instead of silently
|
|
76
|
+
* rewiring the result's prototype to external input.
|
|
77
|
+
*/
|
|
78
|
+
private interpolateDeep(value: any): any {
|
|
79
|
+
if (typeof value === 'string') return this.interpolate(value);
|
|
80
|
+
if (Array.isArray(value)) return value.map((v) => this.interpolateDeep(v));
|
|
81
|
+
if (isPlainObject(value)) {
|
|
82
|
+
return Object.fromEntries(
|
|
83
|
+
Object.entries(value).map(([k, v]) => [this.interpolate(k), this.interpolateDeep(v)])
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
/**
|
|
64
90
|
* Resolve a key to its RAW, uncoerced value (array/object/number kept as-is) — for binding
|
|
65
91
|
* DB query / API request params, where type-strict sinks (MySQL numeric columns, MongoDB,
|
|
@@ -68,7 +94,8 @@ export class TestDataLoader {
|
|
|
68
94
|
* as get()), so an override like `token={{login.body.token}}` or `id={{prefix}}-001` still
|
|
69
95
|
* interpolates — only the final scalar/array/object type is preserved uncoerced. Arrays and
|
|
70
96
|
* objects are returned as-is; refs embedded inside their elements are not interpolated (unlike
|
|
71
|
-
* get()
|
|
97
|
+
* get(), which interpolates every string leaf of a plain container before serializing), since
|
|
98
|
+
* raw() callers bind structured values directly, not text.
|
|
72
99
|
* Throws on missing, mirroring get()'s guard.
|
|
73
100
|
*/
|
|
74
101
|
raw(key: string): any {
|
|
@@ -202,6 +229,26 @@ export class TestDataLoader {
|
|
|
202
229
|
}
|
|
203
230
|
}
|
|
204
231
|
|
|
232
|
+
/**
|
|
233
|
+
* A value get() must serialize as JSON: an array, or a PLAIN object (literal or
|
|
234
|
+
* null-prototype). Class instances at the TOP level (Date, Buffer, ObjectId…,
|
|
235
|
+
* and driver row wrappers like mysql2's RowDataPacket) are NOT containers —
|
|
236
|
+
* they keep the String()/toString() path their existing assertions rely on;
|
|
237
|
+
* nested inside a plain container they serialize through JSON.stringify/toJSON.
|
|
238
|
+
*/
|
|
239
|
+
function isJsonContainer(value: any): boolean {
|
|
240
|
+
return Array.isArray(value) || isPlainObject(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Prototype check only — a `value.constructor === Object` probe is defeated by
|
|
244
|
+
// a body FIELD literally named "constructor" (JSON.parse gives it as an own
|
|
245
|
+
// property), which would silently fall back to "[object Object]" again.
|
|
246
|
+
function isPlainObject(value: any): boolean {
|
|
247
|
+
if (value === null || typeof value !== 'object') return false;
|
|
248
|
+
const proto = Object.getPrototypeOf(value);
|
|
249
|
+
return proto === Object.prototype || proto === null;
|
|
250
|
+
}
|
|
251
|
+
|
|
205
252
|
function loadYamlSync(filePath: string): Record<string, any> | null {
|
|
206
253
|
if (!fs.existsSync(filePath)) return null;
|
|
207
254
|
const content = fs.readFileSync(filePath, 'utf-8');
|