@sun-asterisk/sungen 3.2.31 → 3.2.33
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/cli/commands/audit.d.ts +1 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +13 -2
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +46 -17
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +39 -18
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +14 -7
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts.map +1 -1
- package/dist/cli/commands/results-merge.js +2 -12
- package/dist/cli/commands/results-merge.js.map +1 -1
- package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
- package/dist/cli/commands/results-sanitize.js +2 -13
- package/dist/cli/commands/results-sanitize.js.map +1 -1
- package/dist/cli/commands/setup-mobile-env.d.ts +15 -0
- package/dist/cli/commands/setup-mobile-env.d.ts.map +1 -0
- package/dist/cli/commands/setup-mobile-env.js +65 -0
- package/dist/cli/commands/setup-mobile-env.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +6 -10
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +21 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +32 -1
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/locale-variants.d.ts +7 -1
- package/dist/exporters/locale-variants.d.ts.map +1 -1
- package/dist/exporters/locale-variants.js +23 -16
- package/dist/exporters/locale-variants.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +13 -7
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
- package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
- package/dist/exporters/matrix/carry-manual-results.js +232 -0
- package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +2 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +7 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/wording.d.ts +7 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -1
- package/dist/exporters/matrix/wording.js +27 -1
- package/dist/exporters/matrix/wording.js.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +2 -0
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +7 -2
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
- package/dist/generators/test-generator/code-generator.d.ts +2 -1
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +122 -15
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +70 -4
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +69 -3
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +13 -3
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +16 -10
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +15 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +81 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +187 -13
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +18 -0
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +39 -8
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
- package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +84 -4
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +229 -82
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +35 -8
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability.d.ts +7 -1
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +15 -12
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
- package/dist/harness/config-file.d.ts +17 -0
- package/dist/harness/config-file.d.ts.map +1 -0
- package/dist/harness/config-file.js +100 -0
- package/dist/harness/config-file.js.map +1 -0
- package/dist/harness/depth-lint.d.ts.map +1 -1
- package/dist/harness/depth-lint.js +2 -1
- package/dist/harness/depth-lint.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +6 -2
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/keyword-match.d.ts +59 -0
- package/dist/harness/keyword-match.d.ts.map +1 -0
- package/dist/harness/keyword-match.js +86 -0
- package/dist/harness/keyword-match.js.map +1 -0
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +2 -1
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +42 -7
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/perf.d.ts +7 -1
- package/dist/harness/perf.d.ts.map +1 -1
- package/dist/harness/perf.js +13 -10
- package/dist/harness/perf.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +9 -2
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +16 -4
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -1
- package/dist/harness/read-text.d.ts +6 -0
- package/dist/harness/read-text.d.ts.map +1 -1
- package/dist/harness/read-text.js +7 -1
- package/dist/harness/read-text.js.map +1 -1
- package/dist/harness/secret-scan.d.ts +27 -1
- package/dist/harness/secret-scan.d.ts.map +1 -1
- package/dist/harness/secret-scan.js +57 -4
- package/dist/harness/secret-scan.js.map +1 -1
- package/dist/harness/sensors.d.ts +12 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +41 -18
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-branches.d.ts +7 -20
- package/dist/harness/spec-branches.d.ts.map +1 -1
- package/dist/harness/spec-branches.js +85 -13
- package/dist/harness/spec-branches.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +16 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +131 -12
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +2 -1
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/harness/write-atomic.d.ts +2 -0
- package/dist/harness/write-atomic.d.ts.map +1 -0
- package/dist/harness/write-atomic.js +62 -0
- package/dist/harness/write-atomic.js.map +1 -0
- package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
- package/dist/orchestrator/ai-skills/writer.js +15 -7
- package/dist/orchestrator/ai-skills/writer.js.map +1 -1
- package/dist/orchestrator/appium-environment-setup.d.ts +24 -0
- package/dist/orchestrator/appium-environment-setup.d.ts.map +1 -0
- package/dist/orchestrator/appium-environment-setup.js +157 -0
- package/dist/orchestrator/appium-environment-setup.js.map +1 -0
- package/dist/orchestrator/assets-drift.d.ts.map +1 -1
- package/dist/orchestrator/assets-drift.js +11 -6
- package/dist/orchestrator/assets-drift.js.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +2 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
- package/dist/orchestrator/project-deps-sync.js +22 -1
- package/dist/orchestrator/project-deps-sync.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +9 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +63 -7
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
- package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
- package/dist/orchestrator/security-overrides-catalog.js +63 -0
- package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
- package/dist/orchestrator/security-overrides.d.ts +51 -0
- package/dist/orchestrator/security-overrides.d.ts.map +1 -0
- package/dist/orchestrator/security-overrides.js +165 -0
- package/dist/orchestrator/security-overrides.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
- package/dist/orchestrator/templates/appium-environment/README.md +83 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
- package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
- package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
- package/dist/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
- package/dist/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
- package/dist/orchestrator/templates/appium-environment/setup.ps1 +58 -0
- package/dist/orchestrator/templates/appium-environment/setup.sh +88 -0
- package/dist/orchestrator/templates/specs-checked-state.d.ts +1 -1
- package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-checked-state.js +26 -3
- package/dist/orchestrator/templates/specs-checked-state.js.map +1 -1
- package/dist/orchestrator/templates/specs-checked-state.ts +31 -6
- package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/dist/orchestrator/templates/specs-mock.d.ts +2 -1
- package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-mock.js +10 -3
- package/dist/orchestrator/templates/specs-mock.js.map +1 -1
- package/dist/orchestrator/templates/specs-mock.ts +12 -4
- package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
- package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.js +14 -2
- package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.ts +24 -2
- package/dist/orchestrator/templates/wdio.conf.ts +24 -1
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +12 -3
- package/src/cli/commands/capability.ts +63 -19
- package/src/cli/commands/delivery.ts +38 -16
- package/src/cli/commands/generate.ts +15 -7
- package/src/cli/commands/init.ts +3 -0
- package/src/cli/commands/results-merge.ts +2 -8
- package/src/cli/commands/results-sanitize.ts +2 -9
- package/src/cli/commands/setup-mobile-env.ts +64 -0
- package/src/cli/index.ts +2 -0
- package/src/dashboard/snapshot-builder.ts +6 -8
- package/src/exporters/feature-parser.ts +32 -2
- package/src/exporters/locale-variants.ts +23 -17
- package/src/exporters/matrix/build.ts +14 -8
- package/src/exporters/matrix/carry-manual-results.ts +229 -0
- package/src/exporters/matrix/export.ts +10 -4
- package/src/exporters/matrix/wording.ts +26 -1
- package/src/exporters/xlsx-exporter.ts +7 -2
- package/src/generators/test-generator/adapters/adapter-interface.ts +2 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +94 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +17 -2
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
- package/src/generators/test-generator/code-generator.ts +119 -19
- package/src/generators/test-generator/diagnostics.ts +70 -4
- package/src/generators/test-generator/patterns/index.ts +16 -9
- package/src/generators/test-generator/patterns/types.ts +15 -0
- package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
- package/src/generators/test-generator/step-mapper.ts +196 -13
- package/src/generators/test-generator/template-engine.ts +18 -0
- package/src/generators/test-generator/utils/data-resolver.ts +41 -12
- package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +256 -85
- package/src/harness/audit.ts +41 -12
- package/src/harness/capability.ts +23 -12
- package/src/harness/catalog/universal-viewpoints.yaml +5 -5
- package/src/harness/config-file.ts +66 -0
- package/src/harness/depth-lint.ts +2 -1
- package/src/harness/flow-check.ts +6 -2
- package/src/harness/keyword-match.ts +85 -0
- package/src/harness/next-step.ts +2 -1
- package/src/harness/parse.ts +42 -7
- package/src/harness/perf.ts +15 -7
- package/src/harness/quality-gates.ts +16 -4
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/read-text.ts +7 -1
- package/src/harness/secret-scan.ts +60 -4
- package/src/harness/sensors.ts +50 -19
- package/src/harness/spec-branches.ts +72 -14
- package/src/harness/spec-coverage.ts +133 -13
- package/src/harness/viewpoint-ledger.ts +2 -1
- package/src/harness/write-atomic.ts +22 -0
- package/src/orchestrator/ai-skills/writer.ts +18 -3
- package/src/orchestrator/appium-environment-setup.ts +127 -0
- package/src/orchestrator/assets-drift.ts +10 -5
- package/src/orchestrator/mobile-runtime-scaffolder.ts +2 -0
- package/src/orchestrator/project-deps-sync.ts +23 -1
- package/src/orchestrator/project-initializer.ts +69 -7
- package/src/orchestrator/security-overrides-catalog.ts +69 -0
- package/src/orchestrator/security-overrides.ts +149 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +8 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +64 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
- package/src/orchestrator/templates/appium-environment/README.md +83 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
- package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
- package/src/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
- package/src/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
- package/src/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
- package/src/orchestrator/templates/appium-environment/setup.ps1 +58 -0
- package/src/orchestrator/templates/appium-environment/setup.sh +88 -0
- package/src/orchestrator/templates/specs-checked-state.ts +31 -6
- package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/src/orchestrator/templates/specs-mock.ts +12 -4
- package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/src/orchestrator/templates/specs-url-assert.ts +24 -2
- package/src/orchestrator/templates/wdio.conf.ts +24 -1
|
@@ -46,6 +46,16 @@ export interface UrlExpectation {
|
|
|
46
46
|
declared?: string;
|
|
47
47
|
/** The test-data key behind `declared`, so a failure can name what an author must go fix. */
|
|
48
48
|
dataRef?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The page selector declared `query: any` — the query string is not part of this page's identity,
|
|
51
|
+
* so SILENCE from the step stops meaning "there must be no query" (#748).
|
|
52
|
+
*
|
|
53
|
+
* Emitted only for a page whose entry says so. The default stays strict: a bare page means a bare
|
|
54
|
+
* page, which is the #464 assertion and the reason a filter regression cannot read as green.
|
|
55
|
+
* It is the no-`dataRef` branch alone that this relaxes — a step that declares `with {{v}}` still
|
|
56
|
+
* asserts every param it names, on this page as on any other.
|
|
57
|
+
*/
|
|
58
|
+
ignoreQuery?: boolean;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
/**
|
|
@@ -113,8 +123,14 @@ function locationMatches(pathname: RegExp, hostname: RegExp | undefined, u: URL)
|
|
|
113
123
|
* tracking/UTM param the test never cared about cannot turn it red.
|
|
114
124
|
* A declared fragment is asserted too; a declared value carrying no query
|
|
115
125
|
* at all constrains the path only (the compiler warns: SG-W014).
|
|
126
|
+
*
|
|
127
|
+
* `query: any` on the page's selector entry overrides the FIRST rule only (#748): silence then
|
|
128
|
+
* means "the query is not mine to judge" instead of "there must be none". It is the escape hatch
|
|
129
|
+
* for a page whose query is generated at run time — `/admins/detail?id=<the row just clicked>` —
|
|
130
|
+
* which the strict rule makes unassertable and `with {{v}}` cannot express, since that asserts a
|
|
131
|
+
* literal. The second rule is untouched: declare params and they are still all checked.
|
|
116
132
|
*/
|
|
117
|
-
export function urlMatches({ pathname, hostname, declared, dataRef }: UrlExpectation): (u: URL) => boolean {
|
|
133
|
+
export function urlMatches({ pathname, hostname, declared, dataRef, ignoreQuery }: UrlExpectation): (u: URL) => boolean {
|
|
118
134
|
return (u: URL): boolean => {
|
|
119
135
|
if (!locationMatches(pathname, hostname, u)) return false;
|
|
120
136
|
|
|
@@ -123,7 +139,13 @@ export function urlMatches({ pathname, hostname, declared, dataRef }: UrlExpecta
|
|
|
123
139
|
// undefined, and they mean opposite things: the first is a real assertion (a bare page must be
|
|
124
140
|
// bare), the second is broken test-data. Only the generator knows which, and it says so by
|
|
125
141
|
// emitting `dataRef` exactly when the step carried a `with {{v}}`.
|
|
126
|
-
|
|
142
|
+
// `query: any` on the page entry (#748): the query is not part of this page's identity, so
|
|
143
|
+
// silence from the step is not an assertion about it. Without this there is NO way to express
|
|
144
|
+
// "this path, whatever the query" — `with {{v}}` asserts a LITERAL value, and a detail page
|
|
145
|
+
// keyed by `?id=<the row just clicked>` has no literal to declare. Checked before the bare-page
|
|
146
|
+
// rule, never instead of the declared-value rule below: a step that names params still asserts
|
|
147
|
+
// them here exactly as anywhere else.
|
|
148
|
+
if (!dataRef) return ignoreQuery ? true : u.search === '';
|
|
127
149
|
|
|
128
150
|
// Silence is the whole bug class this assertion exists to kill, so an unusable declared value
|
|
129
151
|
// is an ERROR, not a quiet verdict. Returning false would read as "the app is wrong"; these
|
|
@@ -48,6 +48,14 @@ function evidenceSlug(s: string): string {
|
|
|
48
48
|
.slice(0, 80) || 'unnamed';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* True only in the launcher process. `@wdio/runner` re-parses this very config inside EVERY worker
|
|
53
|
+
* it spawns, so anything logged at module scope prints once per spec unless it is gated. WDIO's own
|
|
54
|
+
* "am I a worker" test is this env var (`@wdio/local-runner` sets it when spawning; `@wdio/utils`
|
|
55
|
+
* reads it) — reuse it rather than inventing a second signal.
|
|
56
|
+
*/
|
|
57
|
+
const IS_LAUNCHER = !process.env.WDIO_WORKER_ID;
|
|
58
|
+
|
|
51
59
|
/**
|
|
52
60
|
* Auto-select MOBILE specs and classify each by its target OS (dual-target routing).
|
|
53
61
|
*
|
|
@@ -82,7 +90,12 @@ function classifyMobileSpecs(root: string): Array<{ file: string; platform: stri
|
|
|
82
90
|
.filter((full) => {
|
|
83
91
|
try {
|
|
84
92
|
return fs.readFileSync(full, 'utf-8').includes('@wdio/globals');
|
|
85
|
-
} catch {
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// A spec listed by readdir that cannot be read cannot be classified, so it is excluded.
|
|
95
|
+
// Excluding it SILENTLY is the fault: WDIO only fails the run when the spec list is
|
|
96
|
+
// EMPTY ("No specs found to run"), so dropping one spec out of ten leaves a green suite
|
|
97
|
+
// containing a test that never executed. Name it, and let the count below show the shortfall.
|
|
98
|
+
if (IS_LAUNCHER) console.warn(`[sungen] could not read ${full} — EXCLUDED from this run: ${(e as Error).message}`);
|
|
86
99
|
return false;
|
|
87
100
|
}
|
|
88
101
|
})
|
|
@@ -98,6 +111,16 @@ const androidSpecs = specEntries.filter((e) => e.platform !== 'ios').map((e) =>
|
|
|
98
111
|
const iosSpecs = specEntries.filter((e) => e.platform !== 'android').map((e) => e.file);
|
|
99
112
|
const mobileSpecs = specEntries.map((e) => e.file); // union — top-level fallback
|
|
100
113
|
|
|
114
|
+
// What DISCOVERY found — a mobile suite is only trustworthy if you can see how many specs it
|
|
115
|
+
// resolved: WDIO fails loudly on an EMPTY list but says nothing when the list is merely short,
|
|
116
|
+
// which is exactly what a dropped spec looks like.
|
|
117
|
+
//
|
|
118
|
+
// Two things this number is not. It is not an error on 0: WDIO already exits 1 there
|
|
119
|
+
// ("No specs found to run"), and a `--shard` that legitimately receives none must still return 0.
|
|
120
|
+
// And under `--shard` it is the PRE-SHARD total, because ConfigParser slices `specs` down to the
|
|
121
|
+
// shard's portion only after this file has finished evaluating.
|
|
122
|
+
if (IS_LAUNCHER) console.log(`[sungen] specs: ${specEntries.length} mobile spec(s) — android ${androidSpecs.length}, ios ${iosSpecs.length}`);
|
|
123
|
+
|
|
101
124
|
// App under test — env overrides the init-configured defaults.
|
|
102
125
|
const APP_PACKAGE = process.env.APP_PACKAGE || '__APP_PACKAGE__';
|
|
103
126
|
const APP_ACTIVITY = process.env.APP_ACTIVITY || '__APP_ACTIVITY__';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.33",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@babel/types": "^7.28.5",
|
|
41
41
|
"@cucumber/gherkin": "^37.0.0",
|
|
42
42
|
"@cucumber/messages": "^31.0.0",
|
|
43
|
-
"@sungen/driver-data-factory": "3.2.
|
|
44
|
-
"@sungen/driver-ui": "3.2.
|
|
43
|
+
"@sungen/driver-data-factory": "3.2.33",
|
|
44
|
+
"@sungen/driver-ui": "3.2.33",
|
|
45
45
|
"chalk": "^5.6.2",
|
|
46
46
|
"commander": "^14.0.2",
|
|
47
47
|
"dotenv": "^17.2.3",
|
|
@@ -7,8 +7,11 @@ import { acceptViewpointBaseline } from '../../harness/viewpoint-baseline';
|
|
|
7
7
|
import { findUnitDir } from '../resolve-unit';
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
function bar(ratio: number, width = 20): string {
|
|
11
|
-
|
|
10
|
+
export function bar(ratio: number, width = 20): string {
|
|
11
|
+
// A display helper must not be able to take the CLI down whatever the model hands it: an
|
|
12
|
+
// out-of-range ratio made `String.repeat` throw `Invalid count value: -5`, which killed the
|
|
13
|
+
// run mid-scorecard so the gate, the findings and the next-step hint never printed (#762).
|
|
14
|
+
const n = Math.max(0, Math.min(width, Math.round((Number.isFinite(ratio) ? ratio : 0) * width)));
|
|
12
15
|
return '█'.repeat(n) + '░'.repeat(width - n);
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -47,6 +50,11 @@ function render(r: AuditReport): void {
|
|
|
47
50
|
L(` ① Viewpoint Gate — page-type: ${r.gate.pageType ?? 'unknown'} — themes ${r.gate.themesCovered}/${r.gate.themesTotal} covered`);
|
|
48
51
|
if (r.gate.gaps.length) for (const g of r.gate.gaps) L(` ✗ ${g.status === 'shallow' ? 'SHALLOW' : 'MISSING'}: ${g.theme}`);
|
|
49
52
|
else L(' ✓ all critical themes covered (with data assertions)');
|
|
53
|
+
// Name the scenario behind each covered theme (#776). A bare count gives a reviewer nothing to
|
|
54
|
+
// check, so a keyword matching something absurd stays invisible; the evidence line makes it
|
|
55
|
+
// obvious. Second line of defence — it holds even if the matcher is wrong again.
|
|
56
|
+
for (const c of r.gate.coveredBy ?? [])
|
|
57
|
+
L(` ✓ ${c.theme} ← [${c.keyword}] ${c.by.length > 88 ? `${c.by.slice(0, 88)}…` : c.by}`);
|
|
50
58
|
L('');
|
|
51
59
|
if (r.flowQuality?.hasContract) {
|
|
52
60
|
const fq = r.flowQuality;
|
|
@@ -109,7 +117,8 @@ function render(r: AuditReport): void {
|
|
|
109
117
|
L(` ⑦ Spec coverage — FR ${r.spec.frCovered}/${r.spec.frTotal} covered [${r.spec.verdict.toUpperCase()}]`);
|
|
110
118
|
for (const g of r.spec.triggerGaps) L(` ✗ TRIGGER-UNCOVERED: "${g.constraint}"${g.code ? ` (${g.code})` : ''} mandated on [${g.required.join(', ')}], only tested on [${g.found.join(', ') || 'none'}] → missing ${g.missing.join(', ')}`);
|
|
111
119
|
for (const u of r.spec.uncoveredMust.slice(0, 6)) L(` ✗ SPEC-UNCOVERED: ${u.id} (MUST) — "${u.text}"`);
|
|
112
|
-
|
|
120
|
+
for (const c of r.spec.clauseGaps.slice(0, 6)) L(` ✗ SPEC-CLAUSE-UNCOVERED: ${c.id} (${c.polarity === 'negate' ? 'MUST NOT' : 'MUST'}) — "${c.text}"`);
|
|
121
|
+
if (!r.spec.triggerGaps.length && !r.spec.uncoveredMust.length && !r.spec.clauseGaps.length) L(' ✓ every MUST FR + per-constraint trigger covered');
|
|
113
122
|
L('');
|
|
114
123
|
}
|
|
115
124
|
if (r.ledger.hasViewpoint && r.ledger.total > 0) {
|
|
@@ -4,33 +4,46 @@ import { createRequire } from 'module';
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import {
|
|
7
|
-
readCapabilities, writeCapabilities, loadDriverCatalog, driverMeta,
|
|
7
|
+
readCapabilities, writeCapabilities, loadDriverCatalog, driverMeta, capabilitiesPath,
|
|
8
|
+
type CapabilityProfile,
|
|
8
9
|
} from '../../harness/capability';
|
|
9
10
|
import { buildPlan, MANUAL_REASONS } from '../../harness/capability-plan';
|
|
10
11
|
import { adapterRegistry } from '../../generators/test-generator/adapters';
|
|
11
12
|
import { emitMobileRuntime, installMobileDriver } from '../../orchestrator/mobile-runtime-scaffolder';
|
|
13
|
+
import { emitAppiumEnvironment, runAppiumEnvironmentSetup } from '../../orchestrator/appium-environment-setup';
|
|
12
14
|
import { coreVersion, resolveDriverVersion } from '../../orchestrator/lockstep-version';
|
|
13
15
|
import { findUnitDir, resolveUnit } from '../resolve-unit';
|
|
16
|
+
import { readTextFile } from '../../harness/read-text';
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
|
-
* Add `test:mobile` to the project's package.json scripts (if absent).
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* Add `test:mobile` to the project's package.json scripts (if absent). Idempotent.
|
|
20
|
+
*
|
|
21
|
+
* Four outcomes, not two. The old boolean collapsed "already present", "no package.json" and
|
|
22
|
+
* "could not parse it" into a single `false`, and the caller rendered that as
|
|
23
|
+
* `– skipped package.json scripts[test:mobile] (already exists)` — which, on a malformed
|
|
24
|
+
* package.json, states the exact opposite of the truth: the script is absent, the command promised
|
|
25
|
+
* it, and the next thing the user sees is `npm error Missing script: "test:mobile"`.
|
|
19
26
|
*/
|
|
20
|
-
|
|
27
|
+
type MobileScriptOutcome =
|
|
28
|
+
| { state: 'added' }
|
|
29
|
+
| { state: 'already-present' }
|
|
30
|
+
| { state: 'no-package-json' }
|
|
31
|
+
| { state: 'unreadable'; error: string };
|
|
32
|
+
|
|
33
|
+
function addMobileScript(cwd: string): MobileScriptOutcome {
|
|
21
34
|
const pkgPath = path.join(cwd, 'package.json');
|
|
22
|
-
if (!fs.existsSync(pkgPath)) return
|
|
35
|
+
if (!fs.existsSync(pkgPath)) return { state: 'no-package-json' };
|
|
23
36
|
let pkg: Record<string, any>;
|
|
24
37
|
try {
|
|
25
|
-
pkg = JSON.parse(
|
|
26
|
-
} catch {
|
|
27
|
-
return
|
|
38
|
+
pkg = JSON.parse(readTextFile(pkgPath)); // BOM-tolerant: npm reads such a file fine, so must we
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { state: 'unreadable', error: (e as Error).message.split('\n')[0] };
|
|
28
41
|
}
|
|
29
42
|
if (!pkg.scripts || typeof pkg.scripts !== 'object') pkg.scripts = {};
|
|
30
|
-
if (pkg.scripts['test:mobile']) return
|
|
43
|
+
if (pkg.scripts['test:mobile']) return { state: 'already-present' };
|
|
31
44
|
pkg.scripts['test:mobile'] = 'wdio run wdio.conf.ts';
|
|
32
45
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
33
|
-
return
|
|
46
|
+
return { state: 'added' };
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
/**
|
|
@@ -157,7 +170,8 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
157
170
|
.description('Enable a driver: install its package (if external) + record it in qa/capabilities.yaml')
|
|
158
171
|
.option('--skip-install', 'Only record in the profile; do not run npm install')
|
|
159
172
|
.option('--force', 'Switch the project platform even if a different one is already set (sungen is one platform per project)')
|
|
160
|
-
.
|
|
173
|
+
.option('--skip-env-setup', 'Mobile: emit appium-environment/ but do not run it now — re-run later: ./appium-environment/setup.sh (Windows: setup.ps1)')
|
|
174
|
+
.action((driver: string, o: { skipInstall?: boolean; force?: boolean; skipEnvSetup?: boolean }) => {
|
|
161
175
|
try {
|
|
162
176
|
const cwd = process.cwd();
|
|
163
177
|
const meta = driverMeta(driver);
|
|
@@ -219,9 +233,13 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
const profile = readCapabilities(cwd);
|
|
236
|
+
assertProfileWritable(profile, cwd);
|
|
222
237
|
const enabled = Array.from(new Set([...profile.enabled, driver]));
|
|
223
238
|
const platform = meta.kind === 'platform' ? driver : profile.platform;
|
|
224
|
-
|
|
239
|
+
// Carry `verification` through. It is the recorded TEST TYPE, not a derived value: dropping
|
|
240
|
+
// it here silently re-derives the scope from `enabled`, handing an E2E-only project the
|
|
241
|
+
// @api/@query verification it had explicitly narrowed away.
|
|
242
|
+
writeCapabilities(cwd, { platform, enabled, verification: profile.verification });
|
|
225
243
|
console.log(`✓ capability "${driver}" enabled${meta.kind === 'platform' ? ` (platform=${driver})` : ''}.`);
|
|
226
244
|
|
|
227
245
|
// Emit the mobile runtime harness when the mobile platform driver is added.
|
|
@@ -234,14 +252,23 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
234
252
|
for (const f of result.skipped) console.log(` – skipped ${f} (already exists)`);
|
|
235
253
|
|
|
236
254
|
// Add test:mobile script to package.json (idempotent)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
255
|
+
const script = addMobileScript(cwd);
|
|
256
|
+
if (script.state === 'added') console.log(' ✓ created package.json scripts[test:mobile]');
|
|
257
|
+
else if (script.state === 'already-present') console.log(' – skipped package.json scripts[test:mobile] (already exists)');
|
|
258
|
+
else if (script.state === 'no-package-json') console.log(' ✗ NOT added package.json scripts[test:mobile] — no package.json here; run `npm init -y` first, then add: "test:mobile": "wdio run wdio.conf.ts"');
|
|
259
|
+
else console.log(` ✗ NOT added package.json scripts[test:mobile] — package.json is not valid JSON (${script.error}); fix it, then add: "test:mobile": "wdio run wdio.conf.ts"`);
|
|
260
|
+
|
|
261
|
+
// appium-environment/ provisioning bundle (idempotent per file) — emitted before the driver
|
|
262
|
+
// install, run after it so its Appium step finds the project-local CLI.
|
|
263
|
+
const env = emitAppiumEnvironment(cwd);
|
|
264
|
+
if (env.created.length > 0) console.log(` ✓ created appium-environment/ (${env.created.length} files)`);
|
|
265
|
+
else console.log(' – skipped appium-environment/ (already exists)');
|
|
242
266
|
|
|
243
267
|
// Best-effort driver install — non-fatal so capability add still succeeds offline.
|
|
244
268
|
installMobileDriver(cwd);
|
|
269
|
+
|
|
270
|
+
// --skip-install means "record only" — then provisioning the machine is not wanted either.
|
|
271
|
+
runAppiumEnvironmentSetup(cwd, { skip: !!(o.skipEnvSetup || o.skipInstall) });
|
|
245
272
|
}
|
|
246
273
|
|
|
247
274
|
console.log('');
|
|
@@ -251,6 +278,22 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
251
278
|
}
|
|
252
279
|
});
|
|
253
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Every path that WRITES qa/capabilities.yaml first reads it, edits one field, and writes the whole
|
|
283
|
+
* file back. That round-trip is only safe when the read succeeded: an unreadable profile reads as
|
|
284
|
+
* "no platform, no drivers", so the write-back would replace the user's declaration with just the
|
|
285
|
+
* one driver being added — and `remove` would replace it with nothing at all. Refuse instead.
|
|
286
|
+
*/
|
|
287
|
+
function assertProfileWritable(profile: CapabilityProfile, cwd: string): void {
|
|
288
|
+
if (profile.source !== 'unreadable') return;
|
|
289
|
+
console.error(
|
|
290
|
+
`✗ ${path.relative(cwd, capabilitiesPath(cwd))} exists but could not be parsed.\n` +
|
|
291
|
+
` Refusing to rewrite it: this command replaces the whole file, so it would discard the\n` +
|
|
292
|
+
` platform, drivers and verification scope you declared. Fix the YAML and re-run.`,
|
|
293
|
+
);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
254
297
|
// ── remove ─────────────────────────────────────────────────────────────
|
|
255
298
|
cap
|
|
256
299
|
.command('remove <driver>')
|
|
@@ -260,9 +303,10 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
260
303
|
try {
|
|
261
304
|
const cwd = process.cwd();
|
|
262
305
|
const profile = readCapabilities(cwd);
|
|
306
|
+
assertProfileWritable(profile, cwd);
|
|
263
307
|
const enabled = profile.enabled.filter((d) => d !== driver);
|
|
264
308
|
const platform = profile.platform === driver ? undefined : profile.platform;
|
|
265
|
-
writeCapabilities(cwd, { platform, enabled });
|
|
309
|
+
writeCapabilities(cwd, { platform, enabled, verification: profile.verification });
|
|
266
310
|
const meta = driverMeta(driver);
|
|
267
311
|
if (o.uninstall && meta && !adapterRegistry.hasAdapter(meta.adapter || meta.id)) {
|
|
268
312
|
spawnSync('npm', ['uninstall', meta.package], { stdio: 'inherit', shell: true });
|
|
@@ -26,7 +26,7 @@ import { loadApiCatalog } from '../../exporters/api-catalog-loader';
|
|
|
26
26
|
import { resolveApiEnvironment } from '../../exporters/api-environment';
|
|
27
27
|
import { resolveWebBaseUrl } from '../../exporters/env-qa';
|
|
28
28
|
import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
|
|
29
|
-
import { scanTestDataSecrets } from '../../harness/secret-scan';
|
|
29
|
+
import { scanTestDataSecrets, formatSecretScan, type SecretScanResult } from '../../harness/secret-scan';
|
|
30
30
|
import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
|
|
31
31
|
import { mapFilePath } from '../../exporters/matrix/map-loader';
|
|
32
32
|
import {
|
|
@@ -39,7 +39,9 @@ import {
|
|
|
39
39
|
hasReview,
|
|
40
40
|
} from '../../exporters/matrix/export';
|
|
41
41
|
import { MatrixFinding } from '../../exporters/matrix/types';
|
|
42
|
+
import { ManualCarryReport } from '../../exporters/matrix/carry-manual-results';
|
|
42
43
|
import { formatTestOutcome, resultTotals } from '../../exporters/matrix/result-totals';
|
|
44
|
+
import { readCapabilities } from '../../harness/capability';
|
|
43
45
|
|
|
44
46
|
const COLOR = {
|
|
45
47
|
reset: '\x1b[0m',
|
|
@@ -375,14 +377,11 @@ function getEnvironment(cwd: string): EnvironmentInfo {
|
|
|
375
377
|
* emits one csv+xlsx per OS and skips the (empty/misleading) base aggregate.
|
|
376
378
|
*/
|
|
377
379
|
function isMobileProject(cwd: string): boolean {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
} catch {
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
380
|
+
// Reads through `readCapabilities` rather than re-parsing the file: the third copy of this parse
|
|
381
|
+
// was the third place a malformed capabilities.yaml quietly read as "web". The shared reader
|
|
382
|
+
// reports the breakage once per process; here it still degrades to web, which is the safe
|
|
383
|
+
// direction (per-OS delivery is skipped, nothing is deleted).
|
|
384
|
+
return readCapabilities(cwd).platform === 'mobile';
|
|
386
385
|
}
|
|
387
386
|
|
|
388
387
|
/**
|
|
@@ -736,11 +735,20 @@ async function exportMatrixTarget(
|
|
|
736
735
|
return null;
|
|
737
736
|
}
|
|
738
737
|
|
|
739
|
-
|
|
738
|
+
let deliverables: Awaited<ReturnType<typeof writeMatrixDeliverables>>;
|
|
739
|
+
try {
|
|
740
|
+
deliverables = await writeMatrixDeliverables(paths, model, opts.format ?? 'xlsx');
|
|
741
|
+
} catch (e) {
|
|
742
|
+
// The existing workbook could not be read, so writing would destroy its Manual results.
|
|
743
|
+
log(` ${COLOR.red}not written:${COLOR.reset} ${(e as Error).message}`);
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
const { csvPath, xlsxPath, manualCarry } = deliverables;
|
|
740
747
|
const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
|
|
741
748
|
const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
|
|
742
749
|
const written = [xlsxPath, csvPath].filter((p): p is string => !!p).map((p) => path.relative(cwd, p));
|
|
743
750
|
for (const rel of written) log(` ${COLOR.green}→ ${rel}${COLOR.reset}`);
|
|
751
|
+
if (manualCarry) printManualCarry(manualCarry);
|
|
744
752
|
return {
|
|
745
753
|
label: paths.label,
|
|
746
754
|
items: model.manifest.itemCount,
|
|
@@ -751,6 +759,23 @@ async function exportMatrixTarget(
|
|
|
751
759
|
};
|
|
752
760
|
}
|
|
753
761
|
|
|
762
|
+
/** What happened to the tester's hand-entered Manual results on this write (#756). */
|
|
763
|
+
function printManualCarry(r: ManualCarryReport): void {
|
|
764
|
+
const kept = r.carried + r.unchecked;
|
|
765
|
+
if (kept + r.reset.length + r.notPlaced.length + r.duplicates.length === 0) return;
|
|
766
|
+
const list = (refs: string[]) => `${refs.slice(0, 4).join(', ')}${refs.length > 4 ? ', …' : ''}`;
|
|
767
|
+
log(` Manual results: ${kept} kept${r.unchecked > 0 ? ` (${r.unchecked} without a drift check — the file predates the fingerprint stamp)` : ''}`);
|
|
768
|
+
if (r.reset.length > 0) {
|
|
769
|
+
log(` ${COLOR.yellow}reset to Pending${COLOR.reset} — procedure changed since the result was recorded (${r.reset.length}): ${list(r.reset)}; the previous result is kept in Note`);
|
|
770
|
+
}
|
|
771
|
+
if (r.notPlaced.length > 0) {
|
|
772
|
+
log(` ${COLOR.yellow}not carried${COLOR.reset} — no matching manual case in the feature any more (${r.notPlaced.length}): ${list(r.notPlaced.map((n) => `${n.ref} was ${n.result}`))}`);
|
|
773
|
+
}
|
|
774
|
+
if (r.duplicates.length > 0) {
|
|
775
|
+
log(` ${COLOR.yellow}not carried${COLOR.reset} — a second row for the same case; only the first was kept (${r.duplicates.length}): ${list(r.duplicates.map((n) => `${n.ref} was ${n.result}`))}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
754
779
|
/**
|
|
755
780
|
* One place that accounts for every target the run did NOT write, so a sweep is
|
|
756
781
|
* never silently partial: scaffolds skipped by preflight and units whose gates /
|
|
@@ -934,12 +959,9 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
934
959
|
// Deliverables are COMMITTED files; delivery is the last stop before a
|
|
935
960
|
// hardcoded credential lands in one. Same scan `generate` runs.
|
|
936
961
|
const scanDirs = [...new Set(toExport.map((t) => qaDir(cwd, t)))];
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
for (const h of secretHits.slice(0, 10)) log(` ${h.file}:${h.line} — ${h.reason}`);
|
|
941
|
-
log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.\n`);
|
|
942
|
-
}
|
|
962
|
+
const secretScan = scanDirs.map((d) => scanTestDataSecrets(d))
|
|
963
|
+
.reduce<SecretScanResult>((a, r) => ({ hits: [...a.hits, ...r.hits], unscanned: [...a.unscanned, ...r.unscanned] }), { hits: [], unscanned: [] });
|
|
964
|
+
for (const line of formatSecretScan(secretScan)) log(line);
|
|
943
965
|
|
|
944
966
|
// 3. Export
|
|
945
967
|
const legacy = options.legacy || options.full;
|
|
@@ -3,7 +3,7 @@ import * as path from 'path';
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import { CodeGenerator } from '../../generators/test-generator/code-generator';
|
|
5
5
|
import { adapterRegistry } from '../../generators/test-generator/adapters';
|
|
6
|
-
import { scanTestDataSecrets } from '../../harness/secret-scan';
|
|
6
|
+
import { scanTestDataSecrets, formatSecretScan, type SecretScanResult } from '../../harness/secret-scan';
|
|
7
7
|
import { capabilityRegistry } from '../../capabilities/registry';
|
|
8
8
|
import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
|
|
9
9
|
import { readCapabilities, writeCapabilities, driverMeta, loadDriverCatalog } from '../../harness/capability';
|
|
@@ -160,6 +160,17 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
160
160
|
const cap = readCapabilities(cwd);
|
|
161
161
|
if (cap.platform) {
|
|
162
162
|
platform = cap.platform;
|
|
163
|
+
} else if (cap.source === 'unreadable') {
|
|
164
|
+
// The file EXISTS and we failed to parse it. Scaffolding the web default here would
|
|
165
|
+
// overwrite whatever the user declared — a mobile project's platform, drivers and
|
|
166
|
+
// verification scope, gone, while the notice claimed the file was missing. Refuse:
|
|
167
|
+
// the one thing we know is that we do not know what this project is.
|
|
168
|
+
throw new Error(
|
|
169
|
+
`qa/capabilities.yaml exists but could not be parsed — fix the YAML and re-run.\n` +
|
|
170
|
+
` Refusing to scaffold a default over it: that would discard the platform, drivers ` +
|
|
171
|
+
`and verification scope it declares.\n` +
|
|
172
|
+
` To compile without reading it, pass --framework <web|mobile|…>.`,
|
|
173
|
+
);
|
|
163
174
|
} else {
|
|
164
175
|
platform = 'web';
|
|
165
176
|
writeCapabilities(cwd, { platform: 'web', enabled: ['web'] });
|
|
@@ -225,12 +236,9 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
225
236
|
if (fs.existsSync(d)) for (const n of fs.readdirSync(d)) scanDirs.push(path.join(d, n));
|
|
226
237
|
}
|
|
227
238
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
for (const h of secretHits.slice(0, 10)) console.log(` ${h.file}:${h.line} — ${h.reason}`);
|
|
232
|
-
console.log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.`);
|
|
233
|
-
}
|
|
239
|
+
const secretScan = scanDirs.map((d) => scanTestDataSecrets(d))
|
|
240
|
+
.reduce<SecretScanResult>((a, r) => ({ hits: [...a.hits, ...r.hits], unscanned: [...a.unscanned, ...r.unscanned] }), { hits: [], unscanned: [] });
|
|
241
|
+
for (const line of formatSecretScan(secretScan)) console.log(line);
|
|
234
242
|
|
|
235
243
|
// Advisory harness sensors (e.g. @cases/@query lint) — run via the Capability SPI,
|
|
236
244
|
// never block generation. Each capability registers its own; the kernel just runs them.
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -13,6 +13,7 @@ export function registerInitCommand(program: Command): void {
|
|
|
13
13
|
.option('--ios-bundle-id <id>', 'iOS bundle id for mobile (e.g. com.example.app.dev) — default app when MOBILE_PLATFORM=ios|both. Used with --mobile')
|
|
14
14
|
.option('--ios-device <name>', "iOS Simulator device name for mobile (default: 'iPhone 16'). Used with --mobile")
|
|
15
15
|
.option('--ios-version <ver>', "iOS Simulator platform version for mobile (default: '18.0'). Used with --mobile")
|
|
16
|
+
.option('--skip-env-setup', 'Mobile: emit appium-environment/ (JDK, Android SDK, Xcode CLT, env vars, Appium drivers) but do not run it now — re-run later: ./appium-environment/setup.sh (Windows: setup.ps1). Used with --mobile')
|
|
16
17
|
.option(
|
|
17
18
|
'--assistant <name>',
|
|
18
19
|
'AI assistant to install for: claude | copilot | codex | all (repeatable). Default: claude',
|
|
@@ -30,6 +31,7 @@ export function registerInitCommand(program: Command): void {
|
|
|
30
31
|
iosBundleId?: string;
|
|
31
32
|
iosDevice?: string;
|
|
32
33
|
iosVersion?: string;
|
|
34
|
+
skipEnvSetup?: boolean;
|
|
33
35
|
assistant?: string[];
|
|
34
36
|
force?: boolean;
|
|
35
37
|
}) => {
|
|
@@ -55,6 +57,7 @@ export function registerInitCommand(program: Command): void {
|
|
|
55
57
|
iosBundleId: options.iosBundleId,
|
|
56
58
|
iosDevice: options.iosDevice,
|
|
57
59
|
iosVersion: options.iosVersion,
|
|
60
|
+
skipEnvSetup: options.skipEnvSetup,
|
|
58
61
|
});
|
|
59
62
|
await initializer.initialize(projectName);
|
|
60
63
|
} catch (error) {
|
|
@@ -13,6 +13,7 @@ import { detectReportRoot, sanitizeReportPaths } from '../../exporters/report-pa
|
|
|
13
13
|
import { registerResultsSanitizeCommand } from './results-sanitize';
|
|
14
14
|
import { resolveResultsPath } from '../../exporters/playwright-report-parser';
|
|
15
15
|
import { parseSpecFile } from '../../exporters/spec-parser';
|
|
16
|
+
import { writeFileAtomic } from '../../harness/write-atomic';
|
|
16
17
|
|
|
17
18
|
interface MergeCliOptions {
|
|
18
19
|
out?: string;
|
|
@@ -125,14 +126,7 @@ export function runResultsMerge(baseArg: string | undefined, newArg: string | un
|
|
|
125
126
|
// that file is the single source of execution truth for delivery/dashboard.
|
|
126
127
|
// (Same-dir rename = same device; concurrent merges are last-rename-wins.)
|
|
127
128
|
const outFile = opts.out ?? baseFile;
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2));
|
|
131
|
-
fs.renameSync(tmp, outFile);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup */ }
|
|
134
|
-
throw err;
|
|
135
|
-
}
|
|
129
|
+
writeFileAtomic(outFile, JSON.stringify(merged, null, 2));
|
|
136
130
|
|
|
137
131
|
console.log(`✓ merged ${path.basename(newFile)} into ${path.relative(cwd, outFile)} — ${replaced.length} updated, ${added.length} added, ${kept.length} kept`);
|
|
138
132
|
return outFile;
|
|
@@ -16,6 +16,7 @@ import * as fs from 'fs';
|
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import fg from 'fast-glob';
|
|
18
18
|
import { detectReportRoot, sanitizeReportPaths } from '../../exporters/report-path-sanitizer';
|
|
19
|
+
import { writeFileAtomic } from '../../harness/write-atomic';
|
|
19
20
|
|
|
20
21
|
/** Where a project keeps the JSON files that carry paths. */
|
|
21
22
|
export const DEFAULT_REPORT_GLOBS = [
|
|
@@ -50,15 +51,7 @@ export function runResultsSanitize(files: string[] | undefined, cwd: string = pr
|
|
|
50
51
|
const root = detectReportRoot(doc) ?? nearestProjectRoot(path.dirname(file)) ?? cwd;
|
|
51
52
|
const { doc: clean, rewritten } = sanitizeReportPaths(doc, { root });
|
|
52
53
|
if (rewritten > 0) {
|
|
53
|
-
|
|
54
|
-
const tmp = `${file}.tmp-${process.pid}`;
|
|
55
|
-
try {
|
|
56
|
-
fs.writeFileSync(tmp, JSON.stringify(clean, null, 2) + (raw.endsWith('\n') ? '\n' : ''));
|
|
57
|
-
fs.renameSync(tmp, file);
|
|
58
|
-
} catch (err) {
|
|
59
|
-
try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort cleanup */ }
|
|
60
|
-
throw err;
|
|
61
|
-
}
|
|
54
|
+
writeFileAtomic(file, JSON.stringify(clean, null, 2) + (raw.endsWith('\n') ? '\n' : ''));
|
|
62
55
|
}
|
|
63
56
|
out.push({ file: path.relative(cwd, file), rewritten });
|
|
64
57
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `sungen setup-mobile-env` — provision this machine for Appium, in a project that already exists.
|
|
5
|
+
*
|
|
6
|
+
* `sungen init --mobile` runs the same bundle, but only the person who created the project ever runs
|
|
7
|
+
* init. The second QA clones a ready-made repo and had no command to reach for: `capability add
|
|
8
|
+
* mobile` re-records a capability that is already recorded, and knowing to run
|
|
9
|
+
* `./appium-environment/setup.sh` by hand assumes they know the folder exists. This is that command.
|
|
10
|
+
*
|
|
11
|
+
* Emits `appium-environment/` when it is missing — a project initialised before the bundle shipped,
|
|
12
|
+
* or one where the folder was deleted — then runs it. Both halves are idempotent, so running it on a
|
|
13
|
+
* fully provisioned machine reports a wall of skips and changes nothing.
|
|
14
|
+
*/
|
|
15
|
+
export function registerSetupMobileEnvCommand(program: Command): void {
|
|
16
|
+
program
|
|
17
|
+
.command('setup-mobile-env')
|
|
18
|
+
.description('Install the Appium environment on THIS machine (JDK, Android SDK, env vars, Xcode CLT, Appium drivers) for an existing mobile project — idempotent, safe to re-run')
|
|
19
|
+
.option('--check', 'Report what is missing and install nothing')
|
|
20
|
+
.option('--skip-android', 'Skip the JDK / Android SDK / uiautomator2 steps')
|
|
21
|
+
.option('--skip-ios', 'Skip the Xcode CLT / applesimutils / xcuitest steps (macOS only)')
|
|
22
|
+
.option('--no-profile', 'Do not write ANDROID_HOME / JAVA_HOME / PATH to your shell profile')
|
|
23
|
+
.option('--emit-only', 'Write appium-environment/ into the project but do not run it')
|
|
24
|
+
.action((o: { check?: boolean; skipAndroid?: boolean; skipIos?: boolean; profile?: boolean; emitOnly?: boolean }) => {
|
|
25
|
+
try {
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
const {
|
|
28
|
+
emitAppiumEnvironment,
|
|
29
|
+
runAppiumEnvironmentSetup,
|
|
30
|
+
appiumEnvironmentRerunCommand,
|
|
31
|
+
} = require('../../orchestrator/appium-environment-setup');
|
|
32
|
+
|
|
33
|
+
// Idempotent per file, so an edited script (a bumped cmdline-tools build, a local hint)
|
|
34
|
+
// survives. A project that already has the folder just sees the skip line.
|
|
35
|
+
const emitted = emitAppiumEnvironment(cwd);
|
|
36
|
+
if (emitted.created.length > 0) {
|
|
37
|
+
console.log(`✓ created appium-environment/ (${emitted.created.length} files)`);
|
|
38
|
+
} else {
|
|
39
|
+
console.log('– skipped appium-environment/ (already present)');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (o.emitOnly) {
|
|
43
|
+
console.log(`\nRun it when you are ready: ${appiumEnvironmentRerunCommand()}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// commander maps `--no-profile` to `profile: false`, so the flag is absent when profile is
|
|
48
|
+
// anything but an explicit false.
|
|
49
|
+
const flags: string[] = [];
|
|
50
|
+
if (o.check) flags.push('--check');
|
|
51
|
+
if (o.skipAndroid) flags.push('--skip-android');
|
|
52
|
+
if (o.skipIos) flags.push('--skip-ios');
|
|
53
|
+
if (o.profile === false) flags.push('--no-profile');
|
|
54
|
+
|
|
55
|
+
const outcome = runAppiumEnvironmentSetup(cwd, { flags });
|
|
56
|
+
// The bundle already printed its own per-step report and summary; exit non-zero so CI and a
|
|
57
|
+
// `&&` chain can act on a machine that is not ready.
|
|
58
|
+
if (outcome === 'failed') process.exitCode = 1;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('❌ setup-mobile-env failed:', error);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { registerInitCommand } from './commands/init';
|
|
|
9
9
|
import { registerAddCommand } from './commands/add';
|
|
10
10
|
import { registerGenerateCommand } from './commands/generate';
|
|
11
11
|
import { registerMakeauthCommand } from './commands/makeauth';
|
|
12
|
+
import { registerSetupMobileEnvCommand } from './commands/setup-mobile-env';
|
|
12
13
|
import { registerUpdateCommand } from './commands/update';
|
|
13
14
|
import { registerDeliveryCommand } from './commands/delivery';
|
|
14
15
|
import { registerFigmaCommand } from './commands/figma';
|
|
@@ -61,6 +62,7 @@ async function main() {
|
|
|
61
62
|
registerAddCommand(program);
|
|
62
63
|
registerGenerateCommand(program);
|
|
63
64
|
registerMakeauthCommand(program);
|
|
65
|
+
registerSetupMobileEnvCommand(program);
|
|
64
66
|
registerUpdateCommand(program);
|
|
65
67
|
registerDeliveryCommand(program);
|
|
66
68
|
registerFigmaCommand(program);
|
|
@@ -29,6 +29,7 @@ import { resolvePlatformAppId } from '../exporters/mobile-app-id';
|
|
|
29
29
|
import { discoverLocaleVariants, LocaleVariant } from '../exporters/locale-variants';
|
|
30
30
|
import { getPackageVersion } from '../exporters/package-info';
|
|
31
31
|
import { ApiCatalogEntry, EnvironmentInfo } from '../exporters/types';
|
|
32
|
+
import { readCapabilities } from '../harness/capability';
|
|
32
33
|
import {
|
|
33
34
|
AggregateSummary,
|
|
34
35
|
DashboardSnapshot,
|
|
@@ -98,14 +99,11 @@ export function buildDashboardSnapshot(options: BuildSnapshotOptions): Dashboard
|
|
|
98
99
|
* Mirrors `isMobileProject` in src/cli/commands/delivery.ts.
|
|
99
100
|
*/
|
|
100
101
|
function isMobileProject(cwd: string): boolean {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} catch {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
102
|
+
// Reads through `readCapabilities` rather than re-parsing the file: the third copy of this parse
|
|
103
|
+
// was the third place a malformed capabilities.yaml quietly read as "web". The shared reader
|
|
104
|
+
// reports the breakage once per process; here it still degrades to web, which is the safe
|
|
105
|
+
// direction (per-OS delivery is skipped, nothing is deleted).
|
|
106
|
+
return readCapabilities(cwd).platform === 'mobile';
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
function buildOneScreen(
|