@sun-asterisk/sungen 3.2.24 → 3.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities/context-router.d.ts.map +1 -1
- package/dist/capabilities/context-router.js +15 -7
- package/dist/capabilities/context-router.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +6 -48
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/changed.d.ts +14 -0
- package/dist/cli/commands/changed.d.ts.map +1 -0
- package/dist/cli/commands/changed.js +128 -0
- package/dist/cli/commands/changed.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +25 -1
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.js +1 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts +19 -0
- package/dist/cli/commands/results-merge.d.ts.map +1 -0
- package/dist/cli/commands/results-merge.js +191 -0
- package/dist/cli/commands/results-merge.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +100 -12
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/api-catalog-loader.d.ts.map +1 -1
- package/dist/exporters/api-catalog-loader.js +10 -3
- package/dist/exporters/api-catalog-loader.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +9 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +55 -3
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +10 -1
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +6 -1
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +10 -0
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.js +1 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +5 -1
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +4 -1
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +7 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +17 -2
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +68 -8
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/results-merge.d.ts +46 -0
- package/dist/exporters/results-merge.d.ts.map +1 -0
- package/dist/exporters/results-merge.js +148 -0
- package/dist/exporters/results-merge.js.map +1 -0
- package/dist/exporters/selector-key-resolver.d.ts +4 -0
- package/dist/exporters/selector-key-resolver.d.ts.map +1 -1
- package/dist/exporters/selector-key-resolver.js +101 -1
- package/dist/exporters/selector-key-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +8 -1
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +5 -4
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +2 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +52 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +20 -10
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -11
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/dist/generators/test-generator/code-generator.d.ts +6 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +80 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +60 -6
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +59 -5
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +7 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.js +36 -2
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +19 -12
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +58 -38
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +23 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts +18 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js +23 -0
- package/dist/generators/test-generator/patterns/unsupported-step-code.js.map +1 -0
- package/dist/generators/test-generator/recipe-expander.js +9 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- package/dist/generators/test-generator/reset-tier.d.ts +68 -0
- package/dist/generators/test-generator/reset-tier.d.ts.map +1 -0
- package/dist/generators/test-generator/reset-tier.js +94 -0
- package/dist/generators/test-generator/reset-tier.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +39 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +144 -5
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +2 -10
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +16 -2
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/format-pattern.d.ts +35 -0
- package/dist/generators/test-generator/utils/format-pattern.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/format-pattern.js +82 -0
- package/dist/generators/test-generator/utils/format-pattern.js.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts +42 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js +142 -0
- package/dist/generators/test-generator/utils/js-literal-scanner.js.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts +18 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js +25 -0
- package/dist/generators/test-generator/utils/mask-refs-for-matching.js.map +1 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +9 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +124 -63
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +20 -1
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts +14 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js +26 -0
- package/dist/generators/test-generator/utils/spec-syntax-check.js.map +1 -0
- package/dist/harness/annotation-overrides.d.ts +0 -12
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +9 -5
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/audit.js +4 -4
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +8 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/capability.d.ts +11 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +8 -0
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +5 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +5 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/run-manifest.d.ts +58 -0
- package/dist/harness/run-manifest.d.ts.map +1 -0
- package/dist/harness/run-manifest.js +264 -0
- package/dist/harness/run-manifest.js.map +1 -0
- package/dist/harness/viewpoint-ledger.d.ts +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +25 -4
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/lockstep-version.d.ts +22 -0
- package/dist/orchestrator/lockstep-version.d.ts.map +1 -0
- package/dist/orchestrator/lockstep-version.js +58 -0
- package/dist/orchestrator/lockstep-version.js.map +1 -0
- package/dist/orchestrator/project-deps-sync.d.ts +39 -0
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -0
- package/dist/orchestrator/project-deps-sync.js +175 -0
- package/dist/orchestrator/project-deps-sync.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +5 -2
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/dist/orchestrator/templates/env.appium.example +10 -0
- package/dist/orchestrator/templates/specs-api.d.ts +4 -2
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +233 -10
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +214 -11
- package/dist/orchestrator/templates/specs-regex-escape.d.ts +13 -0
- package/dist/orchestrator/templates/specs-regex-escape.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.js +19 -0
- package/dist/orchestrator/templates/specs-regex-escape.js.map +1 -0
- package/dist/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/dist/orchestrator/templates/specs-test-data.ts +53 -6
- package/dist/orchestrator/templates/wdio.conf.ts +28 -9
- package/package.json +8 -6
- package/src/capabilities/context-router.ts +13 -5
- package/src/cli/commands/capability.ts +4 -38
- package/src/cli/commands/changed.ts +104 -0
- package/src/cli/commands/delivery.ts +27 -2
- package/src/cli/commands/generate.ts +1 -1
- package/src/cli/commands/results-merge.ts +153 -0
- package/src/cli/commands/update.ts +72 -13
- package/src/cli/index.ts +4 -0
- package/src/exporters/api-catalog-loader.ts +10 -2
- package/src/exporters/api-testcase-formatter.ts +59 -3
- package/src/exporters/csv-exporter.ts +11 -2
- package/src/exporters/json-exporter.ts +7 -2
- package/src/exporters/matrix/build.ts +9 -0
- package/src/exporters/matrix/export.ts +1 -1
- package/src/exporters/matrix/render-csv.ts +5 -1
- package/src/exporters/matrix/render-xlsx.ts +4 -1
- package/src/exporters/matrix/types.ts +7 -0
- package/src/exporters/playwright-report-parser.ts +68 -9
- package/src/exporters/results-merge.ts +150 -0
- package/src/exporters/selector-key-resolver.ts +81 -1
- package/src/exporters/types.ts +8 -1
- package/src/exporters/xlsx-report-builder.ts +5 -4
- package/src/generators/gherkin-parser/index.ts +55 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +23 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/appium/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/date-picker-unsupported.hbs +4 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/lifecycle-missing-app-id.hbs +5 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/matches-format-assertion.hbs +15 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +5 -2
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +185 -26
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +17 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +6 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +18 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/matches-format-assertion.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +4 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element-with-text.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -1
- package/src/generators/test-generator/code-generator.ts +86 -7
- package/src/generators/test-generator/diagnostics.ts +60 -6
- package/src/generators/test-generator/patterns/expect-patterns.ts +37 -2
- package/src/generators/test-generator/patterns/index.ts +58 -33
- package/src/generators/test-generator/patterns/types.ts +36 -0
- package/src/generators/test-generator/patterns/unsupported-step-code.ts +19 -0
- package/src/generators/test-generator/recipe-expander.ts +9 -1
- package/src/generators/test-generator/reset-tier.ts +93 -0
- package/src/generators/test-generator/step-mapper.ts +149 -6
- package/src/generators/test-generator/template-engine.ts +18 -3
- package/src/generators/test-generator/utils/format-pattern.ts +79 -0
- package/src/generators/test-generator/utils/js-literal-scanner.ts +132 -0
- package/src/generators/test-generator/utils/mask-refs-for-matching.ts +21 -0
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +126 -76
- package/src/generators/test-generator/utils/selector-resolver.ts +22 -1
- package/src/generators/test-generator/utils/spec-syntax-check.ts +22 -0
- package/src/harness/annotation-overrides.ts +10 -5
- package/src/harness/audit.ts +4 -4
- package/src/harness/capability-plan.ts +10 -5
- package/src/harness/capability.ts +16 -0
- package/src/harness/catalog/drivers.yaml +5 -0
- package/src/harness/parse.ts +5 -1
- package/src/harness/run-manifest.ts +254 -0
- package/src/harness/viewpoint-ledger.ts +26 -5
- package/src/index.ts +1 -0
- package/src/orchestrator/lockstep-version.ts +46 -0
- package/src/orchestrator/project-deps-sync.ts +175 -0
- package/src/orchestrator/project-initializer.ts +5 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +109 -0
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-api-design/SKILL.md +49 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +14 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +110 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +15 -0
- package/src/orchestrator/templates/env.appium.example +10 -0
- package/src/orchestrator/templates/specs-api.ts +214 -11
- package/src/orchestrator/templates/specs-regex-escape.ts +16 -0
- package/src/orchestrator/templates/specs-test-data.ts +53 -6
- package/src/orchestrator/templates/wdio.conf.ts +28 -9
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-manifest — which scenarios does a QA edit actually oblige us to re-run?
|
|
3
|
+
*
|
|
4
|
+
* The problem it serves: the JSON reporter writes ONE file per target, named only for the unit +
|
|
5
|
+
* platform + env, so a subset re-run overwrites the full run's results. `sungen results merge`
|
|
6
|
+
* restores the lost half, but only if the caller knew to back the file up first — and only after
|
|
7
|
+
* the caller decided WHICH cases to run. This module answers the second question deterministically
|
|
8
|
+
* so `run-test --change-only` is a computation, not a guess.
|
|
9
|
+
*
|
|
10
|
+
* Sibling of `manifest.ts`, deliberately not an extension of it: that one hashes spec.md sections
|
|
11
|
+
* to decide what to REGENERATE, this one hashes the compiled inputs to decide what to RE-RUN. Same
|
|
12
|
+
* conventions (deterministic hashing, `.sungen/` storage, a plan object), different question.
|
|
13
|
+
*
|
|
14
|
+
* The bias is fixed and one-directional: when an edit cannot be attributed to specific scenarios,
|
|
15
|
+
* EVERY scenario is selected. Over-running costs minutes; under-running reports a scenario green
|
|
16
|
+
* that never re-ran after the change that broke it, which is the failure this whole family of
|
|
17
|
+
* guards exists to prevent.
|
|
18
|
+
*
|
|
19
|
+
* Storage: `.sungen/run-manifest/<slug>.json`.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { createHash } from 'crypto';
|
|
24
|
+
import yaml from 'yaml';
|
|
25
|
+
import { featureFilesFor, reportSlug } from './unit-paths';
|
|
26
|
+
import { readTextFile } from './read-text';
|
|
27
|
+
import { GherkinParser } from '../generators/gherkin-parser';
|
|
28
|
+
|
|
29
|
+
const sha = (s: string): string => createHash('sha256').update(s).digest('hex').slice(0, 16);
|
|
30
|
+
|
|
31
|
+
/** Mirrors SelectorResolver.generateKey — a `[Ref]` becomes this before the YAML lookup. */
|
|
32
|
+
const refToKey = (label: string): string => label.normalize('NFC').toLowerCase().trim().replace(/\s+/g, ' ');
|
|
33
|
+
|
|
34
|
+
export interface RunManifest {
|
|
35
|
+
unit: string;
|
|
36
|
+
recordedAt: string;
|
|
37
|
+
/** scenario title → hash of its whole block (tags + steps) */
|
|
38
|
+
scenarios: Record<string, string>;
|
|
39
|
+
/** selectors.yaml key → hash of that entry */
|
|
40
|
+
selectorKeys: Record<string, string>;
|
|
41
|
+
/** test-data key → hash of that value */
|
|
42
|
+
dataKeys: Record<string, string>;
|
|
43
|
+
/** scenario title → the selector keys and data keys its steps touch */
|
|
44
|
+
deps: Record<string, { selectors: string[]; data: string[] }>;
|
|
45
|
+
/** hash of the Background block: a change there reaches every scenario */
|
|
46
|
+
background: string;
|
|
47
|
+
/**
|
|
48
|
+
* PROJECT-level inputs, `qa/<file>` → hash. These reach every scenario of every unit — app.yaml
|
|
49
|
+
* carries widget recipes and the behaviour profile, datasources.yaml the db/api endpoints,
|
|
50
|
+
* capabilities.yaml which drivers even load. Per-key attribution is not possible (a recipe change
|
|
51
|
+
* can alter any step's compilation), so a move here widens to the whole unit. They were the last
|
|
52
|
+
* known under-selection hole: edit app.yaml and the selection used to report "nothing changed".
|
|
53
|
+
*/
|
|
54
|
+
globals: Record<string, string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ChangeSelection {
|
|
58
|
+
unit: string;
|
|
59
|
+
/** scenario titles to re-run, in feature order */
|
|
60
|
+
scenarios: string[];
|
|
61
|
+
/** why each was selected — one short clause per title, for the operator */
|
|
62
|
+
reasons: Record<string, string>;
|
|
63
|
+
/** true when the whole unit was selected because the change could not be attributed */
|
|
64
|
+
all: boolean;
|
|
65
|
+
summary: { total: number; selected: number; firstRun: boolean };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const manifestPath = (cwd: string, unit: string): string =>
|
|
69
|
+
path.join(cwd, '.sungen', 'run-manifest', `${reportSlug(unit)}.json`);
|
|
70
|
+
|
|
71
|
+
/** Flatten a YAML mapping to `key → hash(value)`; a non-mapping file yields one whole-file entry. */
|
|
72
|
+
function hashTopLevel(file: string): Record<string, string> {
|
|
73
|
+
if (!fs.existsSync(file)) return {};
|
|
74
|
+
const text = readTextFile(file);
|
|
75
|
+
let doc: unknown;
|
|
76
|
+
try {
|
|
77
|
+
doc = yaml.parse(text);
|
|
78
|
+
} catch {
|
|
79
|
+
// Unparseable: attribute nothing, and let the caller widen to the whole unit rather than
|
|
80
|
+
// silently treating a broken file as "no change".
|
|
81
|
+
return { __unparseable__: sha(text) };
|
|
82
|
+
}
|
|
83
|
+
// An empty or comment-only file parses to null. That is an EMPTY mapping — nothing to attribute
|
|
84
|
+
// and nothing to widen on — not an unrecognised shape. (A test-data file holding only a comment
|
|
85
|
+
// made the first probe widen to all 18 scenarios.)
|
|
86
|
+
if (doc === null || doc === undefined) return {};
|
|
87
|
+
if (typeof doc !== 'object' || Array.isArray(doc)) return { __whole__: sha(text) };
|
|
88
|
+
const out: Record<string, string> = {};
|
|
89
|
+
for (const [k, v] of Object.entries(doc as Record<string, unknown>)) {
|
|
90
|
+
out[refToKey(k)] = sha(JSON.stringify(v ?? null));
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Read the unit's compiled inputs and fingerprint them. */
|
|
96
|
+
export function buildRunManifest(cwd: string, unitDir: string, unit: string): RunManifest {
|
|
97
|
+
const parser = new GherkinParser();
|
|
98
|
+
const scenarios: Record<string, string> = {};
|
|
99
|
+
const deps: RunManifest['deps'] = {};
|
|
100
|
+
const bgParts: string[] = [];
|
|
101
|
+
|
|
102
|
+
for (const file of featureFilesFor(unitDir, unit)) {
|
|
103
|
+
const parsed = parser.parseFeatureFile(file);
|
|
104
|
+
if (parsed.background?.steps?.length) {
|
|
105
|
+
bgParts.push(parsed.background.steps.map((s) => `${s.keyword} ${s.text}`).join('\n'));
|
|
106
|
+
}
|
|
107
|
+
for (const sc of parsed.scenarios) {
|
|
108
|
+
const block = [...(sc.tags || []), ...sc.steps.map((s) => `${s.keyword} ${s.text}`)].join('\n');
|
|
109
|
+
scenarios[sc.name] = sha(block);
|
|
110
|
+
const sel = new Set<string>();
|
|
111
|
+
const dat = new Set<string>();
|
|
112
|
+
for (const st of sc.steps) {
|
|
113
|
+
if (st.selectorRef) sel.add(refToKey(st.selectorRef));
|
|
114
|
+
if (st.parentRef) sel.add(refToKey(st.parentRef));
|
|
115
|
+
if (st.childRef) sel.add(refToKey(st.childRef));
|
|
116
|
+
if (st.dataRef) dat.add(refToKey(st.dataRef));
|
|
117
|
+
}
|
|
118
|
+
deps[sc.name] = { selectors: [...sel].sort(), data: [...dat].sort() };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const selDir = path.join(unitDir, 'selectors');
|
|
123
|
+
const dataDir = path.join(unitDir, 'test-data');
|
|
124
|
+
const collect = (dir: string): Record<string, string> => {
|
|
125
|
+
if (!fs.existsSync(dir)) return {};
|
|
126
|
+
const merged: Record<string, string> = {};
|
|
127
|
+
for (const f of fs.readdirSync(dir).filter((n) => /\.ya?ml$/.test(n)).sort()) {
|
|
128
|
+
Object.assign(merged, hashTopLevel(path.join(dir, f)));
|
|
129
|
+
}
|
|
130
|
+
return merged;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Project-level config lives beside the units, not inside them.
|
|
134
|
+
const qaDir = path.join(cwd, 'qa');
|
|
135
|
+
const globals: Record<string, string> = {};
|
|
136
|
+
for (const name of ['app.yaml', 'datasources.yaml', 'capabilities.yaml']) {
|
|
137
|
+
const f = path.join(qaDir, name);
|
|
138
|
+
if (fs.existsSync(f)) globals[`qa/${name}`] = sha(readTextFile(f));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
unit,
|
|
143
|
+
recordedAt: new Date().toISOString(),
|
|
144
|
+
scenarios,
|
|
145
|
+
selectorKeys: collect(selDir),
|
|
146
|
+
dataKeys: collect(dataDir),
|
|
147
|
+
deps,
|
|
148
|
+
background: sha(bgParts.join('\n---\n')),
|
|
149
|
+
globals,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function readRunManifest(cwd: string, unit: string): RunManifest | null {
|
|
154
|
+
const p = manifestPath(cwd, unit);
|
|
155
|
+
// Absent baseline is the FIRST-RUN case, not an error — selectChanged widens to the whole unit.
|
|
156
|
+
if (!fs.existsSync(p)) return null;
|
|
157
|
+
try { return JSON.parse(readTextFile(p)) as RunManifest; } catch { return null; }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function writeRunManifest(cwd: string, unit: string, m: RunManifest): string {
|
|
161
|
+
const p = manifestPath(cwd, unit);
|
|
162
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
163
|
+
fs.writeFileSync(p, JSON.stringify(m, null, 2));
|
|
164
|
+
return p;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Keys whose hash differs, was added, or disappeared. */
|
|
168
|
+
function movedKeys(before: Record<string, string>, after: Record<string, string>): Set<string> {
|
|
169
|
+
const moved = new Set<string>();
|
|
170
|
+
for (const k of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
171
|
+
if (before[k] !== after[k]) moved.add(k);
|
|
172
|
+
}
|
|
173
|
+
return moved;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Which scenarios must re-run, given the recorded manifest and the current inputs.
|
|
178
|
+
*
|
|
179
|
+
* A selector key matches a step's `[Ref]` either exactly or as one of the resolver's suffixed
|
|
180
|
+
* forms (`key--button`, `key--3`), because that is how the lookup itself resolves.
|
|
181
|
+
*/
|
|
182
|
+
export function selectChanged(
|
|
183
|
+
before: RunManifest | null,
|
|
184
|
+
after: RunManifest,
|
|
185
|
+
/**
|
|
186
|
+
* Run-affecting files an outside source (git) reports as changed. Passed in rather than read here
|
|
187
|
+
* so this function stays pure and testable; the CLI gathers the evidence. Non-empty + an
|
|
188
|
+
* otherwise-empty selection means something changed that per-key hashing cannot see (a rename, a
|
|
189
|
+
* new file shape) — unattributable, so it widens like the other unattributable cases.
|
|
190
|
+
*/
|
|
191
|
+
externalChanged: string[] = []
|
|
192
|
+
): ChangeSelection {
|
|
193
|
+
const titles = Object.keys(after.scenarios);
|
|
194
|
+
const selectAll = (reason: string): ChangeSelection => ({
|
|
195
|
+
unit: after.unit,
|
|
196
|
+
scenarios: titles,
|
|
197
|
+
reasons: Object.fromEntries(titles.map((t) => [t, reason])),
|
|
198
|
+
all: true,
|
|
199
|
+
summary: { total: titles.length, selected: titles.length, firstRun: !before },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!before) return selectAll('no previous run recorded');
|
|
203
|
+
const movedGlobals = movedKeys(before.globals || {}, after.globals || {});
|
|
204
|
+
if (movedGlobals.size) {
|
|
205
|
+
return selectAll(`project config changed (${[...movedGlobals].join(', ')}) — it reaches every scenario`);
|
|
206
|
+
}
|
|
207
|
+
if (before.background !== after.background) return selectAll('the Background changed — it runs before every scenario');
|
|
208
|
+
if (after.selectorKeys.__unparseable__ || after.dataKeys.__unparseable__) {
|
|
209
|
+
return selectAll('a selectors/test-data file could not be parsed, so the edit cannot be attributed');
|
|
210
|
+
}
|
|
211
|
+
if (after.selectorKeys.__whole__ || after.dataKeys.__whole__) {
|
|
212
|
+
const movedWhole = movedKeys(before.selectorKeys, after.selectorKeys).size
|
|
213
|
+
|| movedKeys(before.dataKeys, after.dataKeys).size;
|
|
214
|
+
if (movedWhole) return selectAll('a selectors/test-data file is not a key mapping, so the edit cannot be attributed');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const movedSel = movedKeys(before.selectorKeys, after.selectorKeys);
|
|
218
|
+
const movedDat = movedKeys(before.dataKeys, after.dataKeys);
|
|
219
|
+
const touches = (key: string, dep: string): boolean => key === dep || key.startsWith(`${dep}--`);
|
|
220
|
+
|
|
221
|
+
const reasons: Record<string, string> = {};
|
|
222
|
+
for (const title of titles) {
|
|
223
|
+
if (before.scenarios[title] !== after.scenarios[title]) {
|
|
224
|
+
reasons[title] = before.scenarios[title] ? 'its steps or tags changed' : 'new scenario';
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const dep = after.deps[title] || { selectors: [], data: [] };
|
|
228
|
+
const sel = [...movedSel].filter((k) => dep.selectors.some((d) => touches(k, d)));
|
|
229
|
+
if (sel.length) { reasons[title] = `selector changed: ${sel.slice(0, 3).join(', ')}`; continue; }
|
|
230
|
+
const dat = [...movedDat].filter((k) => dep.data.some((d) => touches(k, d)));
|
|
231
|
+
if (dat.length) { reasons[title] = `test-data changed: ${dat.slice(0, 3).join(', ')}`; continue; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let selected = titles.filter((t) => reasons[t]);
|
|
235
|
+
|
|
236
|
+
// Git cross-check. Per-key hashing is blind to structural moves — a renamed selectors file, a new
|
|
237
|
+
// file shape, an encoding change — because the keys simply appear or vanish. When an outside
|
|
238
|
+
// source reports run-affecting files changed and we attributed NOTHING, that is the same
|
|
239
|
+
// unattributable case as an unparseable YAML, so it widens rather than reporting "all current".
|
|
240
|
+
if (!selected.length && externalChanged.length) {
|
|
241
|
+
return selectAll(
|
|
242
|
+
`git reports ${externalChanged.length} run-affecting file(s) changed (${externalChanged.slice(0, 3).join(', ')}) `
|
|
243
|
+
+ 'that per-key hashing could not attribute'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
unit: after.unit,
|
|
249
|
+
scenarios: selected,
|
|
250
|
+
reasons,
|
|
251
|
+
all: false,
|
|
252
|
+
summary: { total: titles.length, selected: selected.length, firstRun: false },
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -170,19 +170,39 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
|
|
|
170
170
|
* The language has no reload/back step at all (#593), so the honest outcome is a `@manual`
|
|
171
171
|
* deferral naming the gesture — not a silent swap.
|
|
172
172
|
*/
|
|
173
|
-
const BROWSER_GESTURES: Array<{ gesture: string; re: RegExp; appSubstitute: RegExp }> = [
|
|
173
|
+
const BROWSER_GESTURES: Array<{ gesture: string; re: RegExp; appSubstitute: RegExp; platform?: string }> = [
|
|
174
174
|
{ gesture: 'browser back', re: /\bbrowser(?:'s)?[ -]?(?:back|history)\b|\bhistory[ -]back\b/i,
|
|
175
|
-
appSubstitute: /\bclick \[[^\]]*back[^\]]*\]/i },
|
|
175
|
+
appSubstitute: /\bclick \[[^\]]*back[^\]]*\]/i, platform: 'web' },
|
|
176
176
|
{ gesture: 'browser reload/refresh', re: /\b(?:page |browser )?(?:reload|refresh)(?:ing|ed|es)?\b/i,
|
|
177
|
-
appSubstitute: /\bis on \[[^\]]+\] page\b/i },
|
|
177
|
+
appSubstitute: /\bis on \[[^\]]+\] page\b/i, platform: 'web' },
|
|
178
178
|
{ gesture: 'closing and reopening the tab', re: /\bclos(?:e|ing) (?:and reopen\w*\s*)?the tab\b|\breopen\w* the tab\b/i,
|
|
179
|
-
appSubstitute: /\bis on \[[^\]]+\] page\b/i },
|
|
179
|
+
appSubstitute: /\bis on \[[^\]]+\] page\b/i, platform: 'web' },
|
|
180
|
+
|
|
181
|
+
// The same class on mobile, where the vocabulary is different but the trap is identical: an
|
|
182
|
+
// in-app control is not the OS gesture. Android's hardware/system back does not run the
|
|
183
|
+
// screen's own handler unless the app wires it, and backgrounding can destroy an Activity that
|
|
184
|
+
// an in-app navigation never touches — so a defect that only appears on the OS path lives
|
|
185
|
+
// exactly in the substitution (#626). The check was web-only, so mobile flows were silent about
|
|
186
|
+
// the very risk it exists to catch.
|
|
187
|
+
{ gesture: 'the OS/hardware back gesture', platform: 'mobile',
|
|
188
|
+
re: /\b(?:hardware|system|os|device|android|physical)[ -]?back\b|\bback (?:gesture|button|swipe)\b|\bswipe[ -]back\b/i,
|
|
189
|
+
appSubstitute: /\b(?:tap|click) \[[^\]]*back[^\]]*\]/i },
|
|
190
|
+
{ gesture: 'backgrounding and resuming the app', platform: 'mobile',
|
|
191
|
+
re: /\b(?:background(?:ing|ed)?|foreground(?:ing|ed)?|app switcher|recent apps|minimi[sz]|resum(?:e|ing)|kill(?:ed|ing)? the app|force[ -]stop|process death)\b/i,
|
|
192
|
+
appSubstitute: /\b(?:tap|click) \[[^\]]+\]|is on \[[^\]]+\] screen\b/i },
|
|
193
|
+
{ gesture: 'rotating the device', platform: 'mobile',
|
|
194
|
+
re: /\b(?:rotat(?:e|ing|ion)|landscape|portrait|orientation change)\b/i,
|
|
195
|
+
appSubstitute: /\bis on \[[^\]]+\] screen\b/i },
|
|
180
196
|
];
|
|
181
197
|
|
|
182
198
|
export function browserGestureSubstitutions(
|
|
183
|
-
viewpointPath: string, scenarios: ScenarioInfo[],
|
|
199
|
+
viewpointPath: string, scenarios: ScenarioInfo[], platform?: string,
|
|
184
200
|
): Array<{ gesture: string; item: string }> {
|
|
185
201
|
const out: Array<{ gesture: string; item: string }> = [];
|
|
202
|
+
// A gesture only applies where its platform does: "reload" means nothing on a native app, and
|
|
203
|
+
// "backgrounding" means nothing in a browser tab. An unknown platform checks everything, which
|
|
204
|
+
// is how the web-only set behaved before.
|
|
205
|
+
const applies = (g: { platform?: string }): boolean => !platform || !g.platform || g.platform === platform;
|
|
186
206
|
// Judged PER SCENARIO, not over the whole file: a @manual scenario elsewhere that merely
|
|
187
207
|
// mentions the gesture in passing (explaining a tooling limit) silenced the check for an
|
|
188
208
|
// item that an automated scenario had quietly substituted.
|
|
@@ -190,6 +210,7 @@ export function browserGestureSubstitutions(
|
|
|
190
210
|
const automated = scenarios.filter((s) => !s.manual).map((s) => s.haystack);
|
|
191
211
|
for (const item of parseViewpointItems(viewpointPath)) {
|
|
192
212
|
for (const g of BROWSER_GESTURES) {
|
|
213
|
+
if (!applies(g)) continue;
|
|
193
214
|
if (!g.re.test(item.text)) continue;
|
|
194
215
|
// A @manual scenario whose own subject IS the gesture is the honest answer.
|
|
195
216
|
if (deferred.some((h) => g.re.test(h))) break;
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type { ResolvedAppProfile } from './generators/test-generator/app-profile
|
|
|
20
20
|
export type { MappedStep } from './generators/test-generator/step-mapper';
|
|
21
21
|
export type { ParsedStep } from './generators/gherkin-parser';
|
|
22
22
|
export { getPathCode, inferPath, resolvePathVariables } from './generators/test-generator/utils/path-inference';
|
|
23
|
+
export { formatToRegexSource, formatToRegexLiteral, hasFormatToken } from './generators/test-generator/utils/format-pattern';
|
|
23
24
|
export { LIST_MEMBER_ROLES } from './generators/test-generator/utils/selector-resolver';
|
|
24
25
|
|
|
25
26
|
// --- Precondition-annotation override grammar (shared by the @query / @api driver codegen) ---
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lockstep version resolution for the sungen package family.
|
|
5
|
+
*
|
|
6
|
+
* Core (`@sun-asterisk/sungen`) and every capability driver (`@sungen/driver-*`) are published
|
|
7
|
+
* together, so a project must hold ONE version line across all of them: a driver built against
|
|
8
|
+
* core 3.2.24 loading against core 3.2.20 is a silent-breakage class we refuse to ship. Both
|
|
9
|
+
* `sungen capability add` (first install) and `sungen update` (project sync) resolve the target
|
|
10
|
+
* version through here so they can never disagree.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The running CLI's (core) version — family packages install in lockstep with it (not `@latest`). */
|
|
14
|
+
export function coreVersion(): string {
|
|
15
|
+
try { return require('../../package.json').version || 'latest'; } catch { return 'latest'; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the version to install for a family package, given the running `core` version.
|
|
20
|
+
*
|
|
21
|
+
* Lockstep wants the EXACT `core` version. But the beta channel publishes `<core>-beta.N` before a
|
|
22
|
+
* stable `<core>` ever exists — so a released core (e.g. 3.2.10) asking for `@3.2.10` hits ETARGET
|
|
23
|
+
* out-of-the-box when npm only has `3.2.10-beta.2` (`sungen capability add` then fails for a fresh
|
|
24
|
+
* user). Fall back on the SAME version line: prefer exact `core`, else the highest `core-beta.N`,
|
|
25
|
+
* else the `beta` dist-tag as a last resort. Keeps the driver's pinned core-dep on the same line.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveDriverVersion(pkg: string, core: string): string {
|
|
28
|
+
if (core === 'latest') return 'beta';
|
|
29
|
+
try {
|
|
30
|
+
const out = spawnSync('npm', ['view', pkg, 'versions', '--json'], { encoding: 'utf-8', shell: true });
|
|
31
|
+
if (out.status !== 0 || !out.stdout) return core; // can't query → let the exact install try/fail
|
|
32
|
+
let versions: string[] = [];
|
|
33
|
+
const parsed = JSON.parse(out.stdout);
|
|
34
|
+
versions = Array.isArray(parsed) ? parsed : [parsed];
|
|
35
|
+
if (versions.includes(core)) return core; // exact exists → lockstep
|
|
36
|
+
const prefix = `${core}-beta.`;
|
|
37
|
+
const betas = versions
|
|
38
|
+
.filter((v) => typeof v === 'string' && v.startsWith(prefix))
|
|
39
|
+
.map((v) => parseInt(v.slice(prefix.length), 10))
|
|
40
|
+
.filter((n) => Number.isInteger(n));
|
|
41
|
+
if (betas.length) return `${core}-beta.${Math.max(...betas)}`; // highest same-line prerelease
|
|
42
|
+
return 'beta'; // nothing on this line → newest beta dist-tag
|
|
43
|
+
} catch {
|
|
44
|
+
return core;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { resolveDriverVersion } from './lockstep-version';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Project-local sync of the sungen package family after a global `sungen update`.
|
|
8
|
+
*
|
|
9
|
+
* `sungen update` used to run `npm install -g @sun-asterisk/sungen@<channel>` and stop there. That
|
|
10
|
+
* left every PROJECT-local copy behind, and a project always has them: `sungen capability add <x>`
|
|
11
|
+
* installs `@sungen/driver-<x>` as a devDependency pinned to the core version of the day, and each
|
|
12
|
+
* driver declares `@sun-asterisk/sungen` as a dependency — so a local core lands in the project's
|
|
13
|
+
* node_modules too, along with the `sungen` bin link. The lockfile then pins all of it. Result after
|
|
14
|
+
* an update: global 3.2.24, project still 3.2.20, and `npx sungen` inside the project silently
|
|
15
|
+
* running the OLD compiler — with drivers built against a core that is no longer the one loading
|
|
16
|
+
* them.
|
|
17
|
+
*
|
|
18
|
+
* So the update also reconciles the project: every family package declared (or physically present)
|
|
19
|
+
* in the project is reinstalled on the running core's version line.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Family = core + every capability driver. These MUST share one version line in a project. */
|
|
23
|
+
const CORE_PKG = '@sun-asterisk/sungen';
|
|
24
|
+
const isFamily = (name: string): boolean => name === CORE_PKG || name.startsWith('@sungen/driver-');
|
|
25
|
+
|
|
26
|
+
/** Declared specs pointing at a checkout, not the registry — a workspace/dogfood link we must not clobber. */
|
|
27
|
+
const isLocalSpec = (spec: string): boolean =>
|
|
28
|
+
/^(file:|link:|workspace:|portal:|\.{1,2}\/|\/)/.test(spec) || spec.startsWith('git+') || spec.startsWith('github:');
|
|
29
|
+
|
|
30
|
+
export interface FamilyPkg {
|
|
31
|
+
pkg: string;
|
|
32
|
+
/** Range in package.json dependencies/devDependencies, or null when only present transitively. */
|
|
33
|
+
declared: string | null;
|
|
34
|
+
/** Version actually resolved in the project tree, or null when absent. `link` = symlinked. */
|
|
35
|
+
installed: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DepSyncPlan {
|
|
39
|
+
/** `npm install -D` specs to run. Empty when nothing needs to change. */
|
|
40
|
+
specs: string[];
|
|
41
|
+
/** Family packages found in the project, for reporting. */
|
|
42
|
+
found: FamilyPkg[];
|
|
43
|
+
/** Packages deliberately left alone, with the reason. */
|
|
44
|
+
skipped: Array<{ pkg: string; why: string }>;
|
|
45
|
+
/** Set when there is no project to sync at all (no package.json / no family packages). */
|
|
46
|
+
noop?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read a package.json, returning null when missing or malformed. */
|
|
50
|
+
function readJson(file: string): Record<string, any> | null {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Version of `pkg` as installed in the project tree, or null. Symlinked (workspace) → `link`. */
|
|
59
|
+
function installedVersion(cwd: string, pkg: string): string | null {
|
|
60
|
+
const dir = path.join(cwd, 'node_modules', ...pkg.split('/'));
|
|
61
|
+
try {
|
|
62
|
+
if (fs.lstatSync(dir).isSymbolicLink()) return 'link';
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return readJson(path.join(dir, 'package.json'))?.version ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decide what the project needs so its family packages sit on `core`'s version line.
|
|
71
|
+
*
|
|
72
|
+
* Pure apart from reading the project tree and (for stale packages) asking npm which versions exist
|
|
73
|
+
* on that line — no installs, so `--dry-run` and the tests can plan without side effects.
|
|
74
|
+
*/
|
|
75
|
+
export function planProjectDepSync(
|
|
76
|
+
cwd: string,
|
|
77
|
+
core: string,
|
|
78
|
+
resolve: (pkg: string, core: string) => string = resolveDriverVersion,
|
|
79
|
+
): DepSyncPlan {
|
|
80
|
+
const pkgJson = readJson(path.join(cwd, 'package.json'));
|
|
81
|
+
if (!pkgJson) {
|
|
82
|
+
return { specs: [], found: [], skipped: [], noop: 'no package.json here — nothing project-local to sync' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const declaredAll: Record<string, string> = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
86
|
+
const names = new Set(Object.keys(declaredAll).filter(isFamily));
|
|
87
|
+
|
|
88
|
+
// A transitively-installed core (pulled in by a driver) is the copy that owns the project's `sungen`
|
|
89
|
+
// bin link, so it must be reconciled even though nothing declares it.
|
|
90
|
+
if (!names.has(CORE_PKG) && installedVersion(cwd, CORE_PKG)) names.add(CORE_PKG);
|
|
91
|
+
|
|
92
|
+
if (!names.size) {
|
|
93
|
+
return {
|
|
94
|
+
specs: [],
|
|
95
|
+
found: [],
|
|
96
|
+
skipped: [],
|
|
97
|
+
noop: 'no sungen packages installed in this project — the global update is all that was needed',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const found: FamilyPkg[] = [...names].sort().map((pkg) => ({
|
|
102
|
+
pkg,
|
|
103
|
+
declared: declaredAll[pkg] ?? null,
|
|
104
|
+
installed: installedVersion(cwd, pkg),
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
const specs: string[] = [];
|
|
108
|
+
const skipped: Array<{ pkg: string; why: string }> = [];
|
|
109
|
+
|
|
110
|
+
for (const entry of found) {
|
|
111
|
+
if (entry.declared && isLocalSpec(entry.declared)) {
|
|
112
|
+
skipped.push({ pkg: entry.pkg, why: `declared as ${entry.declared} (local checkout, not the registry)` });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (entry.installed === 'link') {
|
|
116
|
+
skipped.push({ pkg: entry.pkg, why: 'symlinked (workspace or npm link)' });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (entry.installed === core) continue; // already on the running core's exact version
|
|
120
|
+
specs.push(`${entry.pkg}@${resolve(entry.pkg, core)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { specs, found, skipped };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reconcile the project's sungen family packages with the running core version.
|
|
128
|
+
*
|
|
129
|
+
* Runs ONE `npm install -D` for every stale package so the lockfile is rewritten in a single
|
|
130
|
+
* resolution pass. Best-effort by design: a failure here (offline, private registry, no write
|
|
131
|
+
* permission) must not fail `sungen update` — the global install already succeeded, and the user
|
|
132
|
+
* gets the exact command to finish the job by hand.
|
|
133
|
+
*
|
|
134
|
+
* @returns true when the project ended up in sync (or needed nothing), false when the install failed
|
|
135
|
+
*/
|
|
136
|
+
export function syncProjectSungenDeps(cwd: string, core: string, dryRun: boolean): boolean {
|
|
137
|
+
const plan = planProjectDepSync(cwd, core);
|
|
138
|
+
|
|
139
|
+
console.log('🔗 Syncing project-local sungen packages...');
|
|
140
|
+
if (plan.noop) {
|
|
141
|
+
console.log(` – ${plan.noop}\n`);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
for (const s of plan.skipped) console.log(` – skipped ${s.pkg} — ${s.why}`);
|
|
145
|
+
|
|
146
|
+
if (!plan.specs.length) {
|
|
147
|
+
console.log(` ✓ already on ${core}: ${plan.found.map((f) => f.pkg).join(', ')}\n`);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const f of plan.found) {
|
|
152
|
+
if (!plan.specs.some((s) => s.startsWith(`${f.pkg}@`))) continue;
|
|
153
|
+
const wasTransitive = f.declared ? '' : ' (was transitive — pinning explicitly)';
|
|
154
|
+
console.log(` • ${f.pkg} ${f.installed ?? '(not installed)'} → ${core}${wasTransitive}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const cmd = `npm install -D ${plan.specs.join(' ')}`;
|
|
158
|
+
if (dryRun) {
|
|
159
|
+
console.log(` [dry-run] would run: ${cmd}\n`);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(` 📦 ${cmd}`);
|
|
164
|
+
const r = spawnSync('npm', ['install', '-D', ...plan.specs], { cwd, stdio: 'inherit', shell: true });
|
|
165
|
+
if (r.status !== 0) {
|
|
166
|
+
console.warn(
|
|
167
|
+
`⚠️ Project-local sync failed — the GLOBAL sungen is now ${core}, but this project still holds\n` +
|
|
168
|
+
' older copies, so `npx sungen` here would keep running the old compiler. Finish manually:\n' +
|
|
169
|
+
` ${cmd}\n`,
|
|
170
|
+
);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
console.log(` ✓ project-local packages on ${core}\n`);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
@@ -656,9 +656,12 @@ export class ProjectInitializer {
|
|
|
656
656
|
|
|
657
657
|
// Mobile-only drops @playwright/test (no web runner needed). `ajv`/`ajv-formats` back the API
|
|
658
658
|
// Driver's `expect {{name.body}} matches schema [Ref]` (loaded lazily; harmless if unused).
|
|
659
|
+
// fast-xml-parser: the API driver's XML request/response support lazy-requires it
|
|
660
|
+
// from the generated specs/api.ts — installed up front so an `encoding: xml`
|
|
661
|
+
// catalog entry works without a mid-suite MODULE_NOT_FOUND.
|
|
659
662
|
const requiredDeps = this.mobileOnly
|
|
660
|
-
? ['@types/node', 'yaml', 'ajv', 'ajv-formats']
|
|
661
|
-
: ['@playwright/test', '@types/node', 'yaml', 'ajv', 'ajv-formats'];
|
|
663
|
+
? ['@types/node', 'yaml', 'ajv', 'ajv-formats', 'fast-xml-parser']
|
|
664
|
+
: ['@playwright/test', '@types/node', 'yaml', 'ajv', 'ajv-formats', 'fast-xml-parser'];
|
|
662
665
|
|
|
663
666
|
let missingDeps: string[] = requiredDeps;
|
|
664
667
|
try {
|