@sun-asterisk/sungen 3.2.32 → 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 +10 -1
- 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 +35 -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/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/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/label-value-assertion.hbs +8 -2
- 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/test-file.hbs +68 -4
- 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/visible-with-role-variable-assertion.hbs +8 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
- 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 +67 -3
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +33 -4
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +32 -3
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +12 -2
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +15 -9
- 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 +46 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +84 -9
- package/dist/generators/test-generator/step-mapper.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 +37 -4
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +212 -79
- 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 +12 -4
- 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/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 +0 -18
- package/dist/harness/spec-branches.d.ts.map +1 -1
- package/dist/harness/spec-branches.js +23 -2
- package/dist/harness/spec-branches.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +14 -4
- 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/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/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 +8 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +49 -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 +3 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +9 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
- 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 +10 -2
- package/src/cli/commands/capability.ts +51 -18
- package/src/cli/commands/delivery.ts +38 -16
- package/src/cli/commands/generate.ts +15 -7
- package/src/cli/commands/results-merge.ts +2 -8
- package/src/cli/commands/results-sanitize.ts +2 -9
- 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/label-value-assertion.hbs +8 -2
- 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/test-file.hbs +68 -4
- 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/visible-with-role-variable-assertion.hbs +8 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
- package/src/generators/test-generator/code-generator.ts +54 -3
- package/src/generators/test-generator/diagnostics.ts +33 -4
- package/src/generators/test-generator/patterns/index.ts +15 -8
- 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 +90 -8
- 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 +238 -82
- package/src/harness/audit.ts +17 -7
- 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/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 +5 -2
- package/src/harness/spec-coverage.ts +14 -4
- 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/assets-drift.ts +10 -5
- package/src/orchestrator/project-deps-sync.ts +23 -1
- package/src/orchestrator/project-initializer.ts +54 -6
- 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 +3 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +9 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-url-assert.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-url-assert.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"specs-url-assert.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-url-assert.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAYrF;AAwBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,cAAc,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,OAAO,CA6CtH;AAED,oGAAoG;AACpG,KAAK,cAAc,GAAG;IACpB,GAAG,IAAI,MAAM,CAAC;IACd,gBAAgB,CAAC,KAAK,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpF,CAAC;AAEF,uDAAuD;AACvD,UAAU,UAAU;IAAG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE;AAgDlE;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,cAAc,EACpB,WAAW,EAAE,cAAc,EAC3B,IAAI,GAAE,UAAU,GAAG;IAAE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAAO,GACtF,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,cAAc,GAAG;IAAE,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,EAC3G,WAAW,EAAE,cAAc,EAC3B,IAAI,GAAE,UAAU,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAAO,GACtG,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -93,8 +93,14 @@ function locationMatches(pathname, hostname, u) {
|
|
|
93
93
|
* tracking/UTM param the test never cared about cannot turn it red.
|
|
94
94
|
* A declared fragment is asserted too; a declared value carrying no query
|
|
95
95
|
* at all constrains the path only (the compiler warns: SG-W014).
|
|
96
|
+
*
|
|
97
|
+
* `query: any` on the page's selector entry overrides the FIRST rule only (#748): silence then
|
|
98
|
+
* means "the query is not mine to judge" instead of "there must be none". It is the escape hatch
|
|
99
|
+
* for a page whose query is generated at run time — `/admins/detail?id=<the row just clicked>` —
|
|
100
|
+
* which the strict rule makes unassertable and `with {{v}}` cannot express, since that asserts a
|
|
101
|
+
* literal. The second rule is untouched: declare params and they are still all checked.
|
|
96
102
|
*/
|
|
97
|
-
function urlMatches({ pathname, hostname, declared, dataRef }) {
|
|
103
|
+
function urlMatches({ pathname, hostname, declared, dataRef, ignoreQuery }) {
|
|
98
104
|
return (u) => {
|
|
99
105
|
if (!locationMatches(pathname, hostname, u))
|
|
100
106
|
return false;
|
|
@@ -103,8 +109,14 @@ function urlMatches({ pathname, hostname, declared, dataRef }) {
|
|
|
103
109
|
// undefined, and they mean opposite things: the first is a real assertion (a bare page must be
|
|
104
110
|
// bare), the second is broken test-data. Only the generator knows which, and it says so by
|
|
105
111
|
// emitting `dataRef` exactly when the step carried a `with {{v}}`.
|
|
112
|
+
// `query: any` on the page entry (#748): the query is not part of this page's identity, so
|
|
113
|
+
// silence from the step is not an assertion about it. Without this there is NO way to express
|
|
114
|
+
// "this path, whatever the query" — `with {{v}}` asserts a LITERAL value, and a detail page
|
|
115
|
+
// keyed by `?id=<the row just clicked>` has no literal to declare. Checked before the bare-page
|
|
116
|
+
// rule, never instead of the declared-value rule below: a step that names params still asserts
|
|
117
|
+
// them here exactly as anywhere else.
|
|
106
118
|
if (!dataRef)
|
|
107
|
-
return u.search === '';
|
|
119
|
+
return ignoreQuery ? true : u.search === '';
|
|
108
120
|
// Silence is the whole bug class this assertion exists to kill, so an unusable declared value
|
|
109
121
|
// is an ERROR, not a quiet verdict. Returning false would read as "the app is wrong"; these
|
|
110
122
|
// throws say "your test-data is wrong, and here is the key". An empty value used to parse to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-url-assert.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-url-assert.ts"],"names":[],"mappings":";AAAA,oBAAoB;AACpB;;;;;;;;;;;;;;;;;;GAkBG;;
|
|
1
|
+
{"version":3,"file":"specs-url-assert.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-url-assert.ts"],"names":[],"mappings":"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"}
|
|
@@ -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;
|
|
@@ -4,7 +4,8 @@ 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';
|
|
@@ -12,26 +13,37 @@ import { emitMobileRuntime, installMobileDriver } from '../../orchestrator/mobil
|
|
|
12
13
|
import { emitAppiumEnvironment, runAppiumEnvironmentSetup } from '../../orchestrator/appium-environment-setup';
|
|
13
14
|
import { coreVersion, resolveDriverVersion } from '../../orchestrator/lockstep-version';
|
|
14
15
|
import { findUnitDir, resolveUnit } from '../resolve-unit';
|
|
16
|
+
import { readTextFile } from '../../harness/read-text';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
|
-
* Add `test:mobile` to the project's package.json scripts (if absent).
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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"`.
|
|
20
26
|
*/
|
|
21
|
-
|
|
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 {
|
|
22
34
|
const pkgPath = path.join(cwd, 'package.json');
|
|
23
|
-
if (!fs.existsSync(pkgPath)) return
|
|
35
|
+
if (!fs.existsSync(pkgPath)) return { state: 'no-package-json' };
|
|
24
36
|
let pkg: Record<string, any>;
|
|
25
37
|
try {
|
|
26
|
-
pkg = JSON.parse(
|
|
27
|
-
} catch {
|
|
28
|
-
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] };
|
|
29
41
|
}
|
|
30
42
|
if (!pkg.scripts || typeof pkg.scripts !== 'object') pkg.scripts = {};
|
|
31
|
-
if (pkg.scripts['test:mobile']) return
|
|
43
|
+
if (pkg.scripts['test:mobile']) return { state: 'already-present' };
|
|
32
44
|
pkg.scripts['test:mobile'] = 'wdio run wdio.conf.ts';
|
|
33
45
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
34
|
-
return
|
|
46
|
+
return { state: 'added' };
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
/**
|
|
@@ -221,9 +233,13 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
const profile = readCapabilities(cwd);
|
|
236
|
+
assertProfileWritable(profile, cwd);
|
|
224
237
|
const enabled = Array.from(new Set([...profile.enabled, driver]));
|
|
225
238
|
const platform = meta.kind === 'platform' ? driver : profile.platform;
|
|
226
|
-
|
|
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 });
|
|
227
243
|
console.log(`✓ capability "${driver}" enabled${meta.kind === 'platform' ? ` (platform=${driver})` : ''}.`);
|
|
228
244
|
|
|
229
245
|
// Emit the mobile runtime harness when the mobile platform driver is added.
|
|
@@ -236,11 +252,11 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
236
252
|
for (const f of result.skipped) console.log(` – skipped ${f} (already exists)`);
|
|
237
253
|
|
|
238
254
|
// Add test:mobile script to package.json (idempotent)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
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"`);
|
|
244
260
|
|
|
245
261
|
// appium-environment/ provisioning bundle (idempotent per file) — emitted before the driver
|
|
246
262
|
// install, run after it so its Appium step finds the project-local CLI.
|
|
@@ -262,6 +278,22 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
262
278
|
}
|
|
263
279
|
});
|
|
264
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
|
+
|
|
265
297
|
// ── remove ─────────────────────────────────────────────────────────────
|
|
266
298
|
cap
|
|
267
299
|
.command('remove <driver>')
|
|
@@ -271,9 +303,10 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
271
303
|
try {
|
|
272
304
|
const cwd = process.cwd();
|
|
273
305
|
const profile = readCapabilities(cwd);
|
|
306
|
+
assertProfileWritable(profile, cwd);
|
|
274
307
|
const enabled = profile.enabled.filter((d) => d !== driver);
|
|
275
308
|
const platform = profile.platform === driver ? undefined : profile.platform;
|
|
276
|
-
writeCapabilities(cwd, { platform, enabled });
|
|
309
|
+
writeCapabilities(cwd, { platform, enabled, verification: profile.verification });
|
|
277
310
|
const meta = driverMeta(driver);
|
|
278
311
|
if (o.uninstall && meta && !adapterRegistry.hasAdapter(meta.adapter || meta.id)) {
|
|
279
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.
|
|
@@ -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
|
}
|
|
@@ -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(
|
|
@@ -3,9 +3,39 @@
|
|
|
3
3
|
* Reuses the existing GherkinParser.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { GherkinParser, ParsedFeature, ParsedScenario } from '../generators/gherkin-parser';
|
|
6
|
+
import { GherkinParser, ParsedDataTable, ParsedFeature, ParsedScenario } from '../generators/gherkin-parser';
|
|
7
7
|
import { FeatureMetadata, OrderedStep, ScenarioMetadata } from './types';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Fold a step's data table into the text the exporters carry (#771). The table
|
|
11
|
+
* IS the step's content — `tab order:` names every stop, `table match data:`
|
|
12
|
+
* every expected row — so without it a delivered case says "See tab order:."
|
|
13
|
+
* and a table edit leaves the scenario fingerprint unchanged.
|
|
14
|
+
*
|
|
15
|
+
* Folded once, here, so every consumer (matrix + legacy cells, fingerprint,
|
|
16
|
+
* oracle shape) sees the same thing. A step with no table keeps its text
|
|
17
|
+
* byte-identical: no existing fingerprint moves on upgrade.
|
|
18
|
+
*
|
|
19
|
+
* `tab order:` | Ref | [A] | [B] | → `tab order: [A] → [B]` (order is the meaning;
|
|
20
|
+
* `Ref` is a keyword header, not a column name)
|
|
21
|
+
* any other | Name | Age | … → `…: Name: Alice, Age: 30; Name: Bob, Age: 25`
|
|
22
|
+
* (one column too: `Title: x; Title: y` — its header names what x is)
|
|
23
|
+
*
|
|
24
|
+
* A blank cell renders as `(empty)` — the matrix's data-fidelity word. Left blank it would
|
|
25
|
+
* read `Age: ;`, indistinguishable from the truncation this fixes, and in `table match
|
|
26
|
+
* data:` a blank cell is itself an assertion (the cell is empty).
|
|
27
|
+
*/
|
|
28
|
+
export function withDataTable(text: string, table: ParsedDataTable | undefined): string {
|
|
29
|
+
if (!table || table.rows.length === 0) return text;
|
|
30
|
+
const base = text.replace(/\s*:\s*$/, '');
|
|
31
|
+
const cell = (v: string | undefined): string => (v === undefined || v.trim() === '' ? '(empty)' : v);
|
|
32
|
+
// Only `tab order` is a sequence of refs — the one shape matrix/wording.ts reads as `→`.
|
|
33
|
+
const body = /\btab order$/i.test(base)
|
|
34
|
+
? table.rows.map((r) => cell(r.cells[0])).join(' → ')
|
|
35
|
+
: table.rows.map((r) => table.headers.map((h, i) => `${h}: ${cell(r.cells[i])}`).join(', ')).join('; ');
|
|
36
|
+
return `${base}: ${body}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
9
39
|
/**
|
|
10
40
|
* Variables referenced in a scenario: find all {{var_name}} in step text.
|
|
11
41
|
*/
|
|
@@ -60,7 +90,7 @@ function classifySteps(scenario: ParsedScenario): OrderedStep[] {
|
|
|
60
90
|
else if (keyword === 'Then') currentBucket = 'then';
|
|
61
91
|
// And / But → inherit currentBucket
|
|
62
92
|
|
|
63
|
-
ordered.push({ text: step.text, bucket: currentBucket });
|
|
93
|
+
ordered.push({ text: withDataTable(step.text, step.dataTable), bucket: currentBucket });
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
return ordered;
|