@sun-asterisk/sungen 3.2.18 → 3.2.19
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/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +4 -1
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +4 -2
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +10 -1
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +19 -1
- 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 +4 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/dashboard/html-renderer.d.ts.map +1 -1
- package/dist/dashboard/html-renderer.js +7 -2
- package/dist/dashboard/html-renderer.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +3 -0
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/types.d.ts +4 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/api-environment.d.ts.map +1 -1
- package/dist/exporters/api-environment.js +4 -24
- package/dist/exporters/api-environment.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +6 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +11 -4
- 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 +40 -9
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/env-qa.d.ts +13 -0
- package/dist/exporters/env-qa.d.ts.map +1 -0
- package/dist/exporters/env-qa.js +82 -0
- package/dist/exporters/env-qa.js.map +1 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +16 -33
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +57 -20
- 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 +6 -5
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/gates.d.ts.map +1 -1
- package/dist/exporters/matrix/gates.js +3 -2
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/step-buckets.d.ts +13 -0
- package/dist/exporters/matrix/step-buckets.d.ts.map +1 -0
- package/dist/exporters/matrix/step-buckets.js +7 -0
- package/dist/exporters/matrix/step-buckets.js.map +1 -0
- package/dist/exporters/qa-env-secret-denylist.d.ts +43 -0
- package/dist/exporters/qa-env-secret-denylist.d.ts.map +1 -0
- package/dist/exporters/qa-env-secret-denylist.js +143 -0
- package/dist/exporters/qa-env-secret-denylist.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts +20 -13
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +55 -30
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/step-formatter.d.ts +42 -33
- package/dist/exporters/step-formatter.d.ts.map +1 -1
- package/dist/exporters/step-formatter.js +68 -68
- package/dist/exporters/step-formatter.js.map +1 -1
- package/dist/exporters/types.d.ts +9 -8
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +1 -0
- 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/steps/actions/keyboard-global-action.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
- 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/keyboard-global-action.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +70 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +7 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +6 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -1
- package/dist/generators/test-generator/recipe-expander.js +1 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +17 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +108 -17
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +1 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +7 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +8 -4
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +2 -2
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +56 -11
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +18 -2
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +29 -6
- 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 +8 -7
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +2 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/challenge.d.ts.map +1 -1
- package/dist/harness/challenge.js +1 -55
- package/dist/harness/challenge.js.map +1 -1
- package/dist/harness/data-driven-lint.d.ts.map +1 -1
- package/dist/harness/data-driven-lint.js +14 -9
- package/dist/harness/data-driven-lint.js.map +1 -1
- package/dist/harness/depth-lint.d.ts.map +1 -1
- package/dist/harness/depth-lint.js +1 -3
- 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 +9 -7
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +4 -3
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/manifest.js +2 -2
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts +12 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +28 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +52 -30
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/secret-scan.d.ts +3 -1
- package/dist/harness/secret-scan.d.ts.map +1 -1
- package/dist/harness/secret-scan.js +14 -10
- package/dist/harness/secret-scan.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +15 -4
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/trace.js +1 -1
- package/dist/harness/trace.js.map +1 -1
- package/dist/harness/unit-paths.d.ts +16 -0
- package/dist/harness/unit-paths.d.ts.map +1 -1
- package/dist/harness/unit-paths.js +39 -2
- package/dist/harness/unit-paths.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +8 -2
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
- package/dist/orchestrator/templates/playwright.config.ts +33 -2
- package/dist/orchestrator/templates/specs-mock.d.ts +66 -0
- package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-mock.js +137 -0
- package/dist/orchestrator/templates/specs-mock.js.map +1 -0
- package/dist/orchestrator/templates/specs-mock.ts +179 -0
- package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
- package/dist/orchestrator/test-data-guide.js +4 -0
- package/dist/orchestrator/test-data-guide.js.map +1 -1
- package/package.json +3 -3
- package/src/capabilities/discover.ts +4 -1
- package/src/cli/commands/add.ts +4 -2
- package/src/cli/commands/dashboard.ts +10 -1
- package/src/cli/commands/delivery.ts +19 -1
- package/src/cli/commands/generate.ts +4 -1
- package/src/dashboard/html-renderer.ts +8 -2
- package/src/dashboard/snapshot-builder.ts +3 -0
- package/src/dashboard/types.ts +4 -0
- package/src/exporters/api-environment.ts +3 -20
- package/src/exporters/api-testcase-formatter.ts +8 -2
- package/src/exporters/csv-exporter.ts +47 -8
- package/src/exporters/env-qa.ts +41 -0
- package/src/exporters/feature-parser.ts +17 -38
- package/src/exporters/json-exporter.ts +71 -24
- package/src/exporters/matrix/build.ts +6 -5
- package/src/exporters/matrix/gates.ts +3 -2
- package/src/exporters/matrix/step-buckets.ts +15 -0
- package/src/exporters/qa-env-secret-denylist.ts +118 -0
- package/src/exporters/scenario-merger.ts +80 -42
- package/src/exporters/step-formatter.ts +67 -66
- package/src/exporters/types.ts +9 -8
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
- package/src/generators/test-generator/code-generator.ts +82 -7
- package/src/generators/test-generator/diagnostics.ts +7 -1
- package/src/generators/test-generator/recipe-expander.ts +2 -2
- package/src/generators/test-generator/step-mapper.ts +106 -15
- package/src/generators/test-generator/template-engine.ts +2 -2
- package/src/generators/test-generator/utils/data-resolver.ts +8 -5
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +57 -11
- package/src/generators/test-generator/utils/selector-resolver.ts +29 -5
- package/src/harness/audit.ts +10 -9
- package/src/harness/capability-plan.ts +2 -2
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/challenge.ts +2 -3
- package/src/harness/data-driven-lint.ts +13 -9
- package/src/harness/depth-lint.ts +2 -4
- package/src/harness/flow-check.ts +9 -7
- package/src/harness/flow-plan.ts +4 -3
- package/src/harness/manifest.ts +3 -3
- package/src/harness/parse.ts +28 -1
- package/src/harness/script-check.ts +52 -32
- package/src/harness/secret-scan.ts +11 -7
- package/src/harness/sensors.ts +15 -4
- package/src/harness/trace.ts +2 -2
- package/src/harness/unit-paths.ts +39 -2
- package/src/index.ts +1 -0
- package/src/orchestrator/screen-manager.ts +7 -2
- package/src/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
- package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
- package/src/orchestrator/templates/playwright.config.ts +33 -2
- package/src/orchestrator/templates/specs-mock.ts +179 -0
- package/src/orchestrator/test-data-guide.ts +4 -0
|
@@ -20,12 +20,18 @@
|
|
|
20
20
|
* SG-W010 — a row step's [Ref] looks POSITIONAL (First/Last/Top/…) but has no selectors.yaml
|
|
21
21
|
* entry: the label is display-only, so the step checks existence anywhere in the
|
|
22
22
|
* table — an ordering rule would pass even when the row moved
|
|
23
|
+
* SG-W011 — a storage assertion written in `expect … in local/session storage` form:
|
|
24
|
+
* `expect` steps only read `{{response}}` refs, so this compiles to a silent
|
|
25
|
+
* no-op — the supported grammar is `User see [KEY] in local storage exists/…`
|
|
26
|
+
* SG-W012 — a mock-install step AFTER a navigation step in the same block: page.route()
|
|
27
|
+
* registered after goto() misses every request fired during page load — move
|
|
28
|
+
* the install before the navigation (or into Background)
|
|
23
29
|
*
|
|
24
30
|
* Diagnostics never block generation by themselves; `sungen generate --strict`
|
|
25
31
|
* turns any collected diagnostic into a non-zero exit.
|
|
26
32
|
*/
|
|
27
33
|
|
|
28
|
-
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010';
|
|
34
|
+
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012';
|
|
29
35
|
|
|
30
36
|
export interface Diagnostic {
|
|
31
37
|
code: DiagnosticCode;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
export interface RecipeExpandContext {
|
|
22
22
|
/** Pre-rendered locator code for the widget element (dialog/frame scoping included). */
|
|
23
23
|
triggerCode: string;
|
|
24
|
-
/** The step's value — a literal or a
|
|
24
|
+
/** The step's value — a literal or a __SUNGEN_TD$…$__ runtime marker. */
|
|
25
25
|
value: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -53,7 +53,7 @@ function expandPrimitive(prim: unknown, ctx: RecipeExpandContext, itemVar: strin
|
|
|
53
53
|
const targetExpr = (v: unknown): string =>
|
|
54
54
|
v === '$trigger' ? ctx.triggerCode : `page.locator('${escape(String(v))}')`;
|
|
55
55
|
// Resolve a `$value`/`$item`/literal into a JS value EXPRESSION (quoted unless $item;
|
|
56
|
-
// a quoted
|
|
56
|
+
// a quoted __SUNGEN_TD$…$__ marker is rewritten to testData.get(...) by the runtime transform).
|
|
57
57
|
const valueExpr = (v: unknown): string => {
|
|
58
58
|
if (v === '$item') {
|
|
59
59
|
if (!itemVar) throw new Error('$item is only valid inside forEach');
|
|
@@ -43,6 +43,8 @@ export class StepMapper {
|
|
|
43
43
|
private tableRowDeclared: boolean = false;
|
|
44
44
|
/** SG-W007: an action step already ran in this scenario (alert handlers registered after it are too late) */
|
|
45
45
|
private sawActionStep: boolean = false;
|
|
46
|
+
/** SG-W012: a navigation already ran in this scenario (mocks installed after it miss page-load requests) */
|
|
47
|
+
private sawNavigationStep: boolean = false;
|
|
46
48
|
/** Compile-time warning channel (SG-W001/W002); optional — absent means silent legacy behavior */
|
|
47
49
|
private diagnostics?: DiagnosticsCollector;
|
|
48
50
|
/** App profile (qa/app.yaml merged over presets/defaults) — the per-project BEHAVIOR layer */
|
|
@@ -91,6 +93,14 @@ export class StepMapper {
|
|
|
91
93
|
this.featureName = featureName;
|
|
92
94
|
this.featurePath = featurePath;
|
|
93
95
|
this.selectorResolver.setFeatureContext(featureName);
|
|
96
|
+
// One StepMapper serves every feature in a run, so the step counter that names the
|
|
97
|
+
// generated temporaries (`__t_<n>`) used to carry over between files: a screen's spec
|
|
98
|
+
// then depended on how many steps the PRECEDING files happened to contain — i.e. on
|
|
99
|
+
// readdir order and on whether the run was `--all` or `--screen`. Same input, different
|
|
100
|
+
// output, which is exactly what a deterministic compiler must not do (and what made a
|
|
101
|
+
// multi-feature screen report permanent false DRIFT in script-check). The counter only
|
|
102
|
+
// needs to be unique WITHIN one spec file, so it restarts with each feature.
|
|
103
|
+
this.stepCounter = 0;
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
/**
|
|
@@ -137,6 +147,7 @@ export class StepMapper {
|
|
|
137
147
|
this.tableRowScopeSteps = steps.filter((s) => this.isRowScopeEntryStep(s)).length;
|
|
138
148
|
this.tableRowDeclared = false;
|
|
139
149
|
this.sawActionStep = false;
|
|
150
|
+
this.sawNavigationStep = false;
|
|
140
151
|
this.lastPrimaryKeyword = 'Given';
|
|
141
152
|
this.dataResolver.clearCaptured(); // captured vars are scenario-scoped
|
|
142
153
|
this.templateEngine.resetBaseContext();
|
|
@@ -181,6 +192,20 @@ export class StepMapper {
|
|
|
181
192
|
this.sawActionStep = true;
|
|
182
193
|
}
|
|
183
194
|
|
|
195
|
+
// A mock must be routed BEFORE the navigation whose requests it should intercept —
|
|
196
|
+
// page.route() registered after goto() misses everything fired during page load
|
|
197
|
+
// (mirror of the late-alert-listener check above). Background installs compile
|
|
198
|
+
// into beforeEach, before any scenario step, so they never trip this.
|
|
199
|
+
const isMockInstallStep = /\[[^\]]+\]\s+mock\s+is\s+active\b/i.test(step.text);
|
|
200
|
+
if (isMockInstallStep && this.sawNavigationStep) {
|
|
201
|
+
this.diagnostics?.add('SG-W012',
|
|
202
|
+
'mock installed AFTER a navigation step — requests fired during page load were not intercepted. Move the mock step before the navigation, or declare it in Background',
|
|
203
|
+
{ feature: this.featureName, step: step.text });
|
|
204
|
+
} else if (!isMockInstallStep && step.elementType === 'page' &&
|
|
205
|
+
/\b(?:is on|navigates?\s+to|opens?)\b/i.test(step.text)) {
|
|
206
|
+
this.sawNavigationStep = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
184
209
|
// Frame scope directives — intercept before pattern matching
|
|
185
210
|
if (/\bswitch(?:es)?\s+to\b/i.test(step.text) &&
|
|
186
211
|
(step.elementType === 'frame' || step.elementType === 'iframe')) {
|
|
@@ -221,7 +246,14 @@ export class StepMapper {
|
|
|
221
246
|
// Given User is on [...] dialog — set dialog scope (3 cases)
|
|
222
247
|
if (step.text.includes('is on') && step.elementType === 'dialog') {
|
|
223
248
|
this.inDialogScope = true;
|
|
224
|
-
|
|
249
|
+
// setBaseContext MERGES — clear every root key so a previous dialog scope in the
|
|
250
|
+
// same scenario cannot leak its root into this one.
|
|
251
|
+
const contextVars: Record<string, any> = {
|
|
252
|
+
inDialog: true,
|
|
253
|
+
dialogRootExpr: undefined,
|
|
254
|
+
dialogRoleName: undefined,
|
|
255
|
+
dialogFilterText: undefined,
|
|
256
|
+
};
|
|
225
257
|
|
|
226
258
|
if (step.selectorRef && step.dataRef) {
|
|
227
259
|
// Case 3: [panel] dialog with {{value}}
|
|
@@ -234,16 +266,18 @@ export class StepMapper {
|
|
|
234
266
|
filterText = `\${${step.dataRef}}`;
|
|
235
267
|
}
|
|
236
268
|
contextVars.dialogFilterText = filterText;
|
|
269
|
+
// The declared entry still owns the ROOT the filter applies to — but only when it
|
|
270
|
+
// is a non-role strategy (locator/testid/css). A role entry keeps the bare
|
|
271
|
+
// getByRole('dialog') base on purpose here: the name is dynamic, hence the filter.
|
|
272
|
+
const rootExpr = this.renderDialogRoot(step.selectorRef, { nonRoleOnly: true });
|
|
273
|
+
if (rootExpr) contextVars.dialogRootExpr = rootExpr;
|
|
237
274
|
} else if (step.selectorRef) {
|
|
238
|
-
// Case 2: [Title] dialog →
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
roleName = step.selectorRef;
|
|
245
|
-
}
|
|
246
|
-
contextVars.dialogRoleName = roleName;
|
|
275
|
+
// Case 2: [Title] dialog → the root the DECLARED selectors.yaml entry asks for
|
|
276
|
+
// (issue: an explicit `type: locator` / `value: '[role="dialog"]'` was being
|
|
277
|
+
// discarded in favour of getByRole('dialog', { name: <reference text> })).
|
|
278
|
+
const rootExpr = this.renderDialogRoot(step.selectorRef);
|
|
279
|
+
if (rootExpr) contextVars.dialogRootExpr = rootExpr;
|
|
280
|
+
else contextVars.dialogRoleName = step.selectorRef;
|
|
247
281
|
}
|
|
248
282
|
// Case 1: no selectorRef → plain page.getByRole('dialog')
|
|
249
283
|
|
|
@@ -303,16 +337,69 @@ export class StepMapper {
|
|
|
303
337
|
}
|
|
304
338
|
|
|
305
339
|
// No pattern matched - return TODO comment
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
340
|
+
if (/\bexpect\b.*\b(?:local|session)\s+storage\b/i.test(step.text)) {
|
|
341
|
+
// The natural first guess for a storage check — but `expect` steps only read
|
|
342
|
+
// {{response}} refs, so this shape silently compiles to nothing. Point at the
|
|
343
|
+
// supported grammar instead of emitting the generic no-pattern warning.
|
|
344
|
+
this.diagnostics?.add('SG-W011',
|
|
345
|
+
'storage assertions use `User see [KEY] in local storage exists` (also: does not exist / is not empty / has length {{n}} / key matching "PATTERN") — the `expect …` form compiles to a no-op', {
|
|
346
|
+
feature: this.featureName,
|
|
347
|
+
step: step.text,
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
this.diagnostics?.add('SG-W001', 'step matched no pattern — compiled to a no-op // TODO comment', {
|
|
351
|
+
feature: this.featureName,
|
|
352
|
+
step: step.text,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
310
355
|
return {
|
|
311
356
|
code: `// TODO: Implement step: ${step.text}`,
|
|
312
357
|
comment: `Unrecognized step pattern: ${step.text}`,
|
|
313
358
|
};
|
|
314
359
|
}
|
|
315
360
|
|
|
361
|
+
/** Strategies the `locator` partial renders into a usable ROOT expression. Anything else
|
|
362
|
+
* (mobile native strategies) has no meaning as a Playwright dialog root. */
|
|
363
|
+
private static readonly DIALOG_ROOT_STRATEGIES = ['locator', 'testid', 'role', 'id', 'label', 'text', 'placeholder'];
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Render the Playwright root expression for `Given User is on [X] dialog` from the entry
|
|
367
|
+
* DECLARED in selectors.yaml, through the same `locator` partial every other step renders
|
|
368
|
+
* with — so `type: locator`, `type: testid`, a non-`dialog` role, `name: null`, `exact:`
|
|
369
|
+
* and `nth:` mean here exactly what they mean everywhere else. Before this, the entry was
|
|
370
|
+
* read for its `name` only and the root was hardcoded to getByRole('dialog', { name }),
|
|
371
|
+
* silently discarding an explicit `type: locator` / `value: '[role="dialog"]'`.
|
|
372
|
+
*
|
|
373
|
+
* Returns undefined when the reference does not resolve to a usable root — the caller then
|
|
374
|
+
* falls back to the legacy named-dialog root, so behavior is unchanged for those entries.
|
|
375
|
+
*/
|
|
376
|
+
private renderDialogRoot(selectorRef: string, opts: { nonRoleOnly?: boolean } = {}): string | undefined {
|
|
377
|
+
let resolved: any;
|
|
378
|
+
try {
|
|
379
|
+
resolved = this.selectorResolver.resolveSelector(selectorRef, this.featureName, 'dialog', 0);
|
|
380
|
+
} catch {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
if (!resolved) return undefined;
|
|
384
|
+
|
|
385
|
+
// `type: role` + a `locator:` field collapses to strategy 'css', which the locator partial
|
|
386
|
+
// has no case for — normalize it to the raw-locator strategy it actually means.
|
|
387
|
+
if (resolved.strategy === 'css' && resolved.locator) {
|
|
388
|
+
resolved = { ...resolved, strategy: 'locator', value: resolved.locator };
|
|
389
|
+
}
|
|
390
|
+
if (!StepMapper.DIALOG_ROOT_STRATEGIES.includes(resolved.strategy)) return undefined;
|
|
391
|
+
if (opts.nonRoleOnly && resolved.strategy === 'role') return undefined;
|
|
392
|
+
|
|
393
|
+
// Render the root PAGE-rooted: leaving inDialog set (or `scope: dialog` on the entry
|
|
394
|
+
// itself) would send the partial back through dialog-root and nest the dialog in itself.
|
|
395
|
+
const expr = this.templateEngine.renderStep('locator', {
|
|
396
|
+
...resolved,
|
|
397
|
+
scope: resolved.scope === 'dialog' ? undefined : resolved.scope,
|
|
398
|
+
inDialog: false,
|
|
399
|
+
}).trim();
|
|
400
|
+
return expr || undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
316
403
|
/**
|
|
317
404
|
* SG-W002 — a step carried a parsed {{dataRef}} but the matched pattern's generated
|
|
318
405
|
* code never uses it (neither the runtime marker, the ${ref} fallback, nor the
|
|
@@ -323,7 +410,11 @@ export class StepMapper {
|
|
|
323
410
|
private checkDataRefConsumed(step: ParsedStep, mapped: MappedStep): void {
|
|
324
411
|
if (!step.dataRef || !this.diagnostics) return;
|
|
325
412
|
const code = mapped.code || '';
|
|
326
|
-
|
|
413
|
+
// Runtime marker — in both spellings: templates that embed the value into a regex
|
|
414
|
+
// literal run it through the escapeRegex helper, which escapes the marker's `$`
|
|
415
|
+
// delimiters, so the code may carry `__SUNGEN_TD\$key\$__` instead of the plain form.
|
|
416
|
+
const marker = DataResolver.encodeMarker(step.dataRef);
|
|
417
|
+
if (code.includes(marker) || code.includes(marker.replace(/\$/g, '\\$'))) return;
|
|
327
418
|
if (code.includes(`\${${step.dataRef}}`)) return; // unresolved fallback
|
|
328
419
|
if (code.includes(step.dataRef)) return; // testData.set('ref'), comments
|
|
329
420
|
try {
|
|
@@ -277,8 +277,8 @@ export class TemplateEngine {
|
|
|
277
277
|
this.baseContext = { ...this.persistentContext };
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
|
|
281
|
-
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb, needsApi: options?.needsApi });
|
|
280
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean; needsMock?: boolean }): string {
|
|
281
|
+
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb, needsApi: options?.needsApi, needsMock: options?.needsMock });
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
renderTestFile(data: {
|
|
@@ -112,12 +112,15 @@ export class DataResolver {
|
|
|
112
112
|
return { fileName: parts[0], valuePath: parts.slice(1) };
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Encode a test-data ref as an opaque wire marker: `__SUNGEN_TD$<key>$__`,
|
|
117
|
+
* dots escaped to `_D_`. `$` delimiters keep the key boundary unambiguous
|
|
118
|
+
* when the surrounding text continues with underscores (`{{code}}_SUFFIX`)
|
|
119
|
+
* while the marker stays a single JS-identifier-safe token. This is the ONLY
|
|
120
|
+
* producer; the only decoder lives in runtime-data-transformer.ts.
|
|
121
|
+
*/
|
|
115
122
|
static encodeMarker(ref: string): string {
|
|
116
|
-
return `
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
static decodeMarker(encoded: string): string {
|
|
120
|
-
return encoded.replace(/^__SUNGEN_TD_/, '').replace(/__$/, '').replace(/_D_/g, '.');
|
|
123
|
+
return `__SUNGEN_TD$${ref.replace(/\./g, '_D_')}$__`;
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
/**
|
|
@@ -1,8 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Wire format of a runtime test-data marker: `__SUNGEN_TD$<key>$__`.
|
|
3
|
+
*
|
|
4
|
+
* `$` delimits the encoded key on both sides. The key alphabet is [A-Za-z0-9_]
|
|
5
|
+
* (dots in refs are escaped to `_D_` by DataResolver.encodeMarker), so a `$`
|
|
6
|
+
* can never occur INSIDE a key — the `$__` terminator stays unambiguous even
|
|
7
|
+
* when the surrounding literal continues with underscore text. The previous
|
|
8
|
+
* `__SUNGEN_TD_<key>__` format could not tell its `__` terminator apart from a
|
|
9
|
+
* `_`-leading suffix: in `'{{code}}_ACCESS_TOKEN'` the greedy key class
|
|
10
|
+
* swallowed one `_` of the `___` run and decoding produced `get('code_')` plus
|
|
11
|
+
* a detached `ACCESS_TOKEN`. `$` is also identifier-safe — the whole marker
|
|
12
|
+
* still parses as ONE bare JS token, so it survives the bare-identifier
|
|
13
|
+
* (numeric) positions Pass 3 handles.
|
|
14
|
+
*
|
|
15
|
+
* Single source of truth: every pass and every whole-literal probe below is
|
|
16
|
+
* built from these constants. Invariant: every marker producer goes through
|
|
17
|
+
* DataResolver.encodeMarker and every consumer through this module — a
|
|
18
|
+
* drifting hand-copied char class is what let the suffix bug hide.
|
|
19
|
+
*/
|
|
20
|
+
const MARKER_KEY = '[A-Za-z0-9_]+';
|
|
21
|
+
// Templates that embed a value into a regex literal run it through the escapeRegex
|
|
22
|
+
// Handlebars helper first (template-engine.ts), which escapes `$` — so a marker can
|
|
23
|
+
// arrive here spelled `__SUNGEN_TD\$key\$__`. `\\\\?` accepts that optional backslash
|
|
24
|
+
// before each delimiter; the replacement consumes it, so no escape leaks to output.
|
|
25
|
+
const DELIM = '\\\\?\\$';
|
|
26
|
+
/** Marker with no capture group — for "contains a marker" positions inside bigger regexes. */
|
|
27
|
+
const MARKER = `__SUNGEN_TD${DELIM}${MARKER_KEY}${DELIM}__`;
|
|
28
|
+
/** Marker capturing the encoded key. */
|
|
29
|
+
const MARKER_CAP = `__SUNGEN_TD${DELIM}(${MARKER_KEY})${DELIM}__`;
|
|
30
|
+
|
|
31
|
+
const MARKER_PATTERN = new RegExp(MARKER);
|
|
32
|
+
// Pass 0: full-line // comments carrying a marker (anchored — see pass comment).
|
|
33
|
+
const RE_COMMENT_LINE = new RegExp(`^(\\s*)\\/\\/(.*)${MARKER_CAP}(.*)$`, 'gm');
|
|
34
|
+
// Pass 1: a quoted string literal containing at least one marker. Covers ', " and
|
|
35
|
+
// backtick containers; `\\.` lets an ESCAPED quote (`'it\'s {{x}}'`) sit inside the
|
|
36
|
+
// body instead of terminating the literal early. The single-char branch excludes `\`
|
|
37
|
+
// so every backslash is consumed by exactly ONE branch — an ambiguous `(?!\1).`
|
|
38
|
+
// alternative enumerates 2^n segmentations when a marker-less escape-heavy literal
|
|
39
|
+
// (escapeQuotes output: multi-line Flutter content-desc) fails to match, hanging
|
|
40
|
+
// the generate run. `\n` exclusion just makes the dot's no-newline behavior explicit.
|
|
41
|
+
const RE_STRING_LITERAL = new RegExp('([\'"`])((?:\\\\.|(?!\\1)[^\\\\\\n])*?' + MARKER + '(?:\\\\.|(?!\\1)[^\\\\\\n])*)\\1', 'g');
|
|
42
|
+
// Pass 2: a single-line regex literal containing at least one marker.
|
|
43
|
+
const RE_REGEX_LITERAL = new RegExp(`\\/((?:[^/\\\\\\n]|\\\\.)*?${MARKER}(?:[^/\\\\\\n]|\\\\.)*)\\/([gimsuy]*)`, 'g');
|
|
44
|
+
// Whole-literal probe: the body is EXACTLY one marker (bare accessor call, no template).
|
|
45
|
+
const RE_WHOLE_MARKER = new RegExp(`^${MARKER_CAP}$`);
|
|
46
|
+
// Every marker occurrence, capturing the key (Pass 3 + interpolation).
|
|
47
|
+
const RE_MARKER_G = new RegExp(MARKER_CAP, 'g');
|
|
2
48
|
|
|
3
49
|
/**
|
|
4
|
-
* Replace
|
|
5
|
-
*
|
|
50
|
+
* Replace __SUNGEN_TD$…$__ markers with testData.get() calls in generated code.
|
|
51
|
+
* Four passes: comments, string literals, regex literals, then bare identifiers.
|
|
6
52
|
*/
|
|
7
53
|
export function transformToRuntimeData(code: string, accessor: string = 'testData'): string {
|
|
8
54
|
// Pass 0: Comments — replace markers in // comments with decoded key name.
|
|
@@ -14,7 +60,7 @@ export function transformToRuntimeData(code: string, accessor: string = 'testDat
|
|
|
14
60
|
// testData.get() (silent: `toHaveAttribute('href', 'official_site_url')`). Generated
|
|
15
61
|
// comments are always full-line, so anchoring loses nothing.
|
|
16
62
|
code = code.replace(
|
|
17
|
-
|
|
63
|
+
RE_COMMENT_LINE,
|
|
18
64
|
(_, indent, before, enc, after) => `${indent}//${before}${decodeKey(enc)}${after}`
|
|
19
65
|
);
|
|
20
66
|
|
|
@@ -30,22 +76,22 @@ export function transformToRuntimeData(code: string, accessor: string = 'testDat
|
|
|
30
76
|
// 'marker' → testData.get('key')
|
|
31
77
|
// 'a__m1__b__m2__c' → `a${testData.get('k1')}b${testData.get('k2')}c`
|
|
32
78
|
code = code.replace(
|
|
33
|
-
|
|
79
|
+
RE_STRING_LITERAL,
|
|
34
80
|
(_, _quote, body) => {
|
|
35
|
-
const whole =
|
|
81
|
+
const whole = RE_WHOLE_MARKER.exec(body);
|
|
36
82
|
if (whole) return `${accessor}.get('${decodeKey(whole[1])}')`;
|
|
37
83
|
return `\`${interpolateMarkers(body, accessor)}\``;
|
|
38
84
|
}
|
|
39
85
|
);
|
|
40
86
|
|
|
41
|
-
// Pass 2: Regex literal context —
|
|
87
|
+
// Pass 2: Regex literal context — /…$marker$…/ → new RegExp(`…`). Same
|
|
42
88
|
// several-markers-per-literal rule as Pass 1 (/{{min}}-{{max}} items/).
|
|
43
89
|
// Only matches within a single line (no newlines in the body).
|
|
44
90
|
code = code.replace(
|
|
45
|
-
|
|
91
|
+
RE_REGEX_LITERAL,
|
|
46
92
|
(_, body, flags) => {
|
|
47
93
|
const flagStr = flags ? `, '${flags}'` : '';
|
|
48
|
-
const whole =
|
|
94
|
+
const whole = RE_WHOLE_MARKER.exec(body);
|
|
49
95
|
if (whole) return `new RegExp(${accessor}.get('${decodeKey(whole[1])}')${flagStr})`;
|
|
50
96
|
return `new RegExp(\`${interpolateMarkers(body, accessor)}\`${flagStr})`;
|
|
51
97
|
}
|
|
@@ -55,7 +101,7 @@ export function transformToRuntimeData(code: string, accessor: string = 'testDat
|
|
|
55
101
|
// sits as a bare token in a numeric position (e.g. `toHaveCount(__marker__)` from the
|
|
56
102
|
// table/list count templates). testData.get() returns a string, so coerce with Number().
|
|
57
103
|
code = code.replace(
|
|
58
|
-
|
|
104
|
+
RE_MARKER_G,
|
|
59
105
|
(_, enc) => `Number(${accessor}.get('${decodeKey(enc)}'))`
|
|
60
106
|
);
|
|
61
107
|
|
|
@@ -72,5 +118,5 @@ function decodeKey(encoded: string): string {
|
|
|
72
118
|
|
|
73
119
|
/** Replace every marker in a literal's body with a `${accessor.get('key')}` interpolation. */
|
|
74
120
|
function interpolateMarkers(body: string, accessor: string): string {
|
|
75
|
-
return body.replace(
|
|
121
|
+
return body.replace(RE_MARKER_G, (_, enc) => `\${${accessor}.get('${decodeKey(enc)}')}`);
|
|
76
122
|
}
|
|
@@ -56,6 +56,23 @@ interface SelectorEntry {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Roles whose accessible name comes from their own CONTENT, not from a label — so a name the
|
|
61
|
+
* compiler invented from the `[Reference]` text can never match a real element.
|
|
62
|
+
*
|
|
63
|
+
* The `name:` key is defaulted to the Gherkin reference when an entry omits it, which is right for
|
|
64
|
+
* label-named roles (button/link/textbox) and wrong for these. Left unchecked it compiles to
|
|
65
|
+
* `getByRole('option', { name: 'Shop Item' })` — zero elements — and `toBeHidden()` PASSES on an
|
|
66
|
+
* empty locator, so the scenario goes green without ever testing anything.
|
|
67
|
+
*
|
|
68
|
+
* The rule lives HERE, at the point the name is invented, rather than in each consumer: it used to
|
|
69
|
+
* be enforced in exactly one of the branches that read `resolved.name`, and every other branch
|
|
70
|
+
* (`is hidden`, `is disabled`) silently kept the bogus filter.
|
|
71
|
+
*
|
|
72
|
+
* An EXPLICIT `name:` in selectors.yaml is always honoured — the user knows their DOM.
|
|
73
|
+
*/
|
|
74
|
+
export const LIST_MEMBER_ROLES = new Set(['listitem', 'option', 'cell']);
|
|
75
|
+
|
|
59
76
|
// New selector file structure: flat key-value pairs
|
|
60
77
|
type SelectorFile = Record<string, SelectorEntry>;
|
|
61
78
|
|
|
@@ -275,7 +292,9 @@ export class SelectorResolver {
|
|
|
275
292
|
'list': () => ({ strategy: 'role', role: 'list', name: label, value: 'list' }),
|
|
276
293
|
'listitem': () => ({ strategy: 'role', role: 'listitem', value: 'listitem' }),
|
|
277
294
|
'searchbox': () => ({ strategy: 'role', role: 'searchbox', name: label, value: 'searchbox' }),
|
|
278
|
-
|
|
295
|
+
// no `name` — a list-member role names itself from its content (see LIST_MEMBER_ROLES);
|
|
296
|
+
// `listitem` already omitted it, `option`/`cell` used to disagree.
|
|
297
|
+
'option': () => ({ strategy: 'role', role: 'option', value: 'option' }),
|
|
279
298
|
'slider': () => ({ strategy: 'role', role: 'slider', name: label, value: 'slider' }),
|
|
280
299
|
'switch': () => ({ strategy: 'role', role: 'switch', name: label, value: 'switch' }),
|
|
281
300
|
'tab': () => ({ strategy: 'role', role: 'tab', name: label, value: 'tab' }),
|
|
@@ -286,7 +305,7 @@ export class SelectorResolver {
|
|
|
286
305
|
'progressbar': () => ({ strategy: 'role', role: 'progressbar', name: label, value: 'progressbar' }),
|
|
287
306
|
'region': () => ({ strategy: 'role', role: 'region', name: label, value: 'region' }),
|
|
288
307
|
'article': () => ({ strategy: 'role', role: 'article', name: label, value: 'article' }),
|
|
289
|
-
'cell': () => ({ strategy: 'role', role: 'cell',
|
|
308
|
+
'cell': () => ({ strategy: 'role', role: 'cell', value: 'cell' }),
|
|
290
309
|
'status': () => ({ strategy: 'role', role: 'status', name: label, value: 'status' }),
|
|
291
310
|
'navigation': () => ({ strategy: 'role', role: 'navigation', name: label, value: 'navigation' }),
|
|
292
311
|
};
|
|
@@ -491,8 +510,8 @@ export class SelectorResolver {
|
|
|
491
510
|
* Replace {{variable}} references in selector values with runtime data markers.
|
|
492
511
|
* This enables i18n: selectors can reference locale-dependent text from test-data.
|
|
493
512
|
*
|
|
494
|
-
* "{{lbl_submit}}" → "
|
|
495
|
-
* "Hello {{name}}" → "Hello
|
|
513
|
+
* "{{lbl_submit}}" → "__SUNGEN_TD$lbl_submit$__"
|
|
514
|
+
* "Hello {{name}}" → "Hello __SUNGEN_TD$name$__"
|
|
496
515
|
* "No variables" → "No variables" (unchanged)
|
|
497
516
|
*/
|
|
498
517
|
private static resolveTemplateVars(value: string): string {
|
|
@@ -517,7 +536,12 @@ export class SelectorResolver {
|
|
|
517
536
|
// Explicit `name: null` in YAML is a deliberate opt-out (e.g. a role selector that must NOT
|
|
518
537
|
// filter by accessible name); it must not be conflated with "unset". The role template omits
|
|
519
538
|
// `{ name }` when this resolves to a falsy value.
|
|
520
|
-
|
|
539
|
+
// A list-member role gets NO defaulted name — see LIST_MEMBER_ROLES. `type` is the entry's
|
|
540
|
+
// declared kind and `rawValue` the role, so the check reads the entry, not a resolved shape.
|
|
541
|
+
const defaultsNameFromLabel = !(type === 'role' && LIST_MEMBER_ROLES.has(String(rawValue)));
|
|
542
|
+
const rawName = entry.name === undefined
|
|
543
|
+
? (defaultsNameFromLabel ? originalLabel : '')
|
|
544
|
+
: (entry.name ?? '');
|
|
521
545
|
const value = SelectorResolver.resolveTemplateVars(rawValue);
|
|
522
546
|
const name = SelectorResolver.resolveTemplateVars(rawName);
|
|
523
547
|
const nth = entry.nth !== undefined && entry.nth !== null ? entry.nth : undefined;
|
package/src/harness/audit.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
|
-
import {
|
|
11
|
+
import { loadUnitScenarios, readUnitFeatureText, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
|
|
12
12
|
import { checkViewpointBaseline, ViewpointBaseline } from './viewpoint-baseline';
|
|
13
|
-
import {
|
|
13
|
+
import { featureFilesFor } from './unit-paths';
|
|
14
14
|
import {
|
|
15
15
|
loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
|
|
16
16
|
automatableManual, flowCoveredThemes, flowRegressionDepth, oracleStrength,
|
|
@@ -117,9 +117,10 @@ export function seniorBandedOverall(
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
120
|
+
// A unit is a DIRECTORY of features — `add --feature` puts sub-features beside the canonical
|
|
121
|
+
// one, and they all count. `featureFiles[0]` is that canonical `<basename>.feature` (an api
|
|
122
|
+
// flow `flows/<flow>` lives at `<dir>/features/<flow>.feature`, not `features/flows/…`).
|
|
123
|
+
const featureFiles = featureFilesFor(screenDir, screenName);
|
|
123
124
|
const viewpointPath = path.join(screenDir, 'requirements', 'test-viewpoint.md');
|
|
124
125
|
// Catalog-resolution id (for the @api/@query gate sensors): the unit's path relative to qa/ —
|
|
125
126
|
// `flows/<flow>`, `api/<area>`, `api/flows/<flow>`, else the bare screen. A bare screen matches
|
|
@@ -127,9 +128,9 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
127
128
|
const catalogScreenName = catalogIdFromScreenDir(screenDir);
|
|
128
129
|
|
|
129
130
|
const specPath = path.join(screenDir, 'requirements', 'spec.md');
|
|
130
|
-
const featureText =
|
|
131
|
+
const featureText = readUnitFeatureText(screenDir, screenName);
|
|
131
132
|
|
|
132
|
-
const scenarios: ScenarioInfo[] =
|
|
133
|
+
const scenarios: ScenarioInfo[] = loadUnitScenarios(screenDir, screenName);
|
|
133
134
|
const viewpoints: ViewpointEntry[] = parseViewpointOverview(viewpointPath);
|
|
134
135
|
// AO-1 — capability-routed scoring: the viewpoint catalog + score-bearing gate are owned by the
|
|
135
136
|
// unit's capability, resolved from the unit id (`api/<area>` → `api`; screen/flow → the default
|
|
@@ -390,14 +391,14 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
390
391
|
}
|
|
391
392
|
// TQ-9 — manual reason-code mismatch: an explicit @manual:Mx that disagrees with the reason
|
|
392
393
|
// text makes the capability planner recommend the wrong driver. Surface it so the code is fixed.
|
|
393
|
-
for (const mm of manualReasonMismatches
|
|
394
|
+
for (const mm of featureFiles.flatMap(manualReasonMismatches).slice(0, 8)) {
|
|
394
395
|
const label = MANUAL_REASONS[mm.inferred]?.label ?? (mm.inferred === 'XS' ? 'Cross-screen (→ flow)' : mm.inferred);
|
|
395
396
|
findings.push(`MANUAL-REASON-MISMATCH: "${mm.scenario}" is tagged @manual:${mm.explicit} but its reason reads as ${mm.inferred} (${label}) → fix the code so \`sungen capability plan\` recommends the right driver to automate it.`);
|
|
396
397
|
}
|
|
397
398
|
// TQ-10 — surface the Capability Planner recommendation (recommend-only; never installs). Silenced
|
|
398
399
|
// by `capability_suggestions: off` in qa/context.md. Reuses the planner (trustworthy after TQ-9).
|
|
399
400
|
if (intent.capabilitySuggestions) {
|
|
400
|
-
const plan = buildPlan(screenDir,
|
|
401
|
+
const plan = buildPlan(screenDir, screenName);
|
|
401
402
|
if (plan.recommendations.length) {
|
|
402
403
|
const recs = plan.recommendations.map((r) => `\`sungen capability add ${r.driver}\` (automates ${r.count})`).join(' · ');
|
|
403
404
|
findings.push(`CAPABILITY-SUGGESTION: ${plan.capabilityManual} @manual scenario(s) are capability-manual (a driver could automate them) — ${recs}. Recommend-only: nothing is installed automatically; the ${plan.judgmentManual} judgment-manual (M6/M8/M9) correctly stay manual.`);
|
|
@@ -10,6 +10,7 @@ import * as fs from 'fs';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { loadDriverCatalog } from './capability';
|
|
12
12
|
import { readTextFile } from './read-text';
|
|
13
|
+
import { featureFilesFor } from './unit-paths';
|
|
13
14
|
|
|
14
15
|
export type ReasonClass = 'capability' | 'keep' | 'flow';
|
|
15
16
|
export interface ReasonDef { code: string; label: string; cls: ReasonClass; drivers: string[] }
|
|
@@ -193,8 +194,7 @@ export interface CapabilityPlan {
|
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
export function buildPlan(screenDir: string, screenName: string): CapabilityPlan {
|
|
196
|
-
const
|
|
197
|
-
const scenarios = parseScenarios(featurePath);
|
|
197
|
+
const scenarios = featureFilesFor(screenDir, screenName).flatMap(parseScenarios);
|
|
198
198
|
const catalog = loadDriverCatalog();
|
|
199
199
|
|
|
200
200
|
const modes: Record<string, number> = {};
|
package/src/harness/challenge.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* The AI agent layer adds the semantic + novelty judgement on top of this spine.
|
|
19
19
|
*/
|
|
20
20
|
import * as path from 'path';
|
|
21
|
-
import {
|
|
21
|
+
import { loadUnitScenarios, ScenarioInfo } from './parse';
|
|
22
22
|
import { runAudit } from './audit';
|
|
23
23
|
|
|
24
24
|
export interface ChallengeFinding {
|
|
@@ -63,8 +63,7 @@ const NOVELTY_LENSES = [
|
|
|
63
63
|
];
|
|
64
64
|
|
|
65
65
|
export function buildChallenge(screenDir: string, screenName: string): ChallengeReport {
|
|
66
|
-
const
|
|
67
|
-
const scenarios: ScenarioInfo[] = loadScenarios(featurePath);
|
|
66
|
+
const scenarios: ScenarioInfo[] = loadUnitScenarios(screenDir, screenName);
|
|
68
67
|
const audit = runAudit(screenDir, screenName);
|
|
69
68
|
|
|
70
69
|
// 1. Coverage critic — over-covered buckets (secondary >> business-core) + shallow themes.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
+
import { featureFilesFor } from './unit-paths';
|
|
11
12
|
import { parse as parseYaml } from 'yaml';
|
|
12
13
|
import { GherkinParser, ParsedScenario } from '../generators/gherkin-parser';
|
|
13
14
|
import { resolveQuery, lintCatalog } from './query-catalog';
|
|
@@ -44,15 +45,18 @@ export function lintDataDriven(screenDir: string, cwd: string = process.cwd()):
|
|
|
44
45
|
const base = path.basename(screenDir);
|
|
45
46
|
const isFlow = path.basename(path.dirname(screenDir)) === 'flows';
|
|
46
47
|
const screenName = isFlow ? `flows/${base}` : base;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
// Every `.feature` of the unit — `add --feature` sub-features lint too. A file that
|
|
49
|
+
// fails to parse contributes nothing rather than voiding the whole unit's lint.
|
|
50
|
+
const scenarios: ParsedScenario[] = featureFilesFor(screenDir, base)
|
|
51
|
+
.filter((p) => fs.existsSync(p))
|
|
52
|
+
.flatMap((p) => {
|
|
53
|
+
try {
|
|
54
|
+
return new GherkinParser().parseFeatureFile(p).scenarios || [];
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (scenarios.length === 0) return [];
|
|
56
60
|
|
|
57
61
|
const tdPath = path.join(screenDir, 'test-data', `${base}.yaml`);
|
|
58
62
|
const td: Record<string, any> = tdPath && fs.existsSync(tdPath) ? parseYaml(fs.readFileSync(tdPath, 'utf8')) || {} : {};
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import {
|
|
16
|
+
import { loadUnitScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
|
|
17
17
|
import { loadCatalog, viewpointGate, assertionDepth, dataThemesFor, CatalogTheme } from './sensors';
|
|
18
18
|
|
|
19
19
|
export type DepthAction = 'deepen' | 'defer';
|
|
@@ -50,11 +50,9 @@ function matchTheme(s: ScenarioInfo, dataThemes: CatalogTheme[]): CatalogTheme |
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function runDepthLint(screenDir: string, screenName: string, focus = 'functional'): DepthLintReport {
|
|
53
|
-
const last = screenName.split('/').pop() || screenName;
|
|
54
|
-
const featurePath = path.join(screenDir, 'features', `${last}.feature`);
|
|
55
53
|
const viewpointPath = path.join(screenDir, 'requirements', 'test-viewpoint.md');
|
|
56
54
|
|
|
57
|
-
const scenarios: ScenarioInfo[] =
|
|
55
|
+
const scenarios: ScenarioInfo[] = loadUnitScenarios(screenDir, screenName);
|
|
58
56
|
const viewpoints: ViewpointEntry[] = parseViewpointOverview(viewpointPath);
|
|
59
57
|
const catalog = loadCatalog();
|
|
60
58
|
const gate = viewpointGate(scenarios, viewpoints, catalog);
|