@sun-asterisk/sungen 3.2.13 → 3.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +29 -50
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +3 -46
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/exporters/locale-variants.d.ts +38 -0
- package/dist/exporters/locale-variants.d.ts.map +1 -0
- package/dist/exporters/locale-variants.js +159 -0
- package/dist/exporters/locale-variants.js.map +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +11 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
- package/dist/generators/test-generator/code-generator.d.ts +10 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +41 -4
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +6 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +5 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +6 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +41 -2
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/harness/annotation-overrides.d.ts +4 -1
- package/dist/harness/annotation-overrides.d.ts.map +1 -1
- package/dist/harness/annotation-overrides.js +5 -2
- package/dist/harness/annotation-overrides.js.map +1 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +5 -0
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/orchestrator/flow-manager.d.ts.map +1 -1
- package/dist/orchestrator/flow-manager.js +4 -3
- package/dist/orchestrator/flow-manager.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +4 -7
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +8 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +6 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +1 -0
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +3 -1
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +3 -1
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +3 -1
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +3 -1
- package/dist/orchestrator/templates/specs-test-data.ts +77 -5
- package/dist/orchestrator/test-data-guide.d.ts +8 -0
- package/dist/orchestrator/test-data-guide.d.ts.map +1 -0
- package/dist/orchestrator/test-data-guide.js +193 -0
- package/dist/orchestrator/test-data-guide.js.map +1 -0
- package/package.json +4 -4
- package/src/cli/commands/delivery.ts +32 -70
- package/src/dashboard/snapshot-builder.ts +11 -61
- package/src/exporters/locale-variants.ts +157 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +11 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
- package/src/generators/test-generator/code-generator.ts +36 -1
- package/src/generators/test-generator/diagnostics.ts +6 -1
- package/src/generators/test-generator/step-mapper.ts +43 -2
- package/src/harness/annotation-overrides.ts +5 -2
- package/src/harness/quality-gates.ts +4 -0
- package/src/orchestrator/flow-manager.ts +4 -3
- package/src/orchestrator/screen-manager.ts +4 -7
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +8 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +6 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +9 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +1 -0
- package/src/orchestrator/templates/specs-api.ts +3 -1
- package/src/orchestrator/templates/specs-db.ts +3 -1
- package/src/orchestrator/templates/specs-test-data.ts +77 -5
- package/src/orchestrator/test-data-guide.ts +158 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && mkdir -p dist/generators/test-generator/adapters/appium/templates/steps && cp -r src/generators/test-generator/adapters/appium/templates/*.hbs dist/generators/test-generator/adapters/appium/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/appium/templates/steps dist/generators/test-generator/adapters/appium/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
|
|
13
13
|
"build:dashboard": "cd ../../dashboard && npm install --silent && npm run build && cd - && cp ../../dashboard/dist/index.html src/dashboard/templates/index.html",
|
|
14
14
|
"dev": "tsx src/cli/index.ts",
|
|
15
|
-
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/exporter/delivery-preflight-selectors.run.ts && tsx tests/exporter/delivery-prerun-overview.run.ts && tsx tests/exporter/manual-testcase-testdata.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/codegen/steps-base-not-compiled.run.ts && tsx tests/codegen/manual-oracle-gate.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/runtime/test-data-array-projection.run.ts &&tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/cosmos-engine.run.ts && tsx tests/db-runtime/mongodb-engine.run.ts && tsx tests/db-runtime/dynamodb-engine.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.ts && tsx tests/db-runtime/mongodb-integration.opt-in.run.ts && tsx tests/db-runtime/dynamodb-integration.opt-in.run.ts",
|
|
15
|
+
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/exporter/delivery-preflight-selectors.run.ts && tsx tests/exporter/delivery-prerun-overview.run.ts && tsx tests/exporter/delivery-locale-env-discovery.run.ts && tsx tests/exporter/manual-testcase-testdata.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/codegen/steps-base-not-compiled.run.ts && tsx tests/codegen/manual-oracle-gate.run.ts && tsx tests/codegen/locator-value-qualified-assertions.run.ts && tsx tests/codegen/table-match-data-column-mapping.run.ts && tsx tests/codegen/param-binding-native-type.run.ts && tsx tests/codegen/table-row-scope-redeclaration.run.ts && tsx tests/codegen/table-row-label-resolution.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/runtime/test-data-array-projection.run.ts && tsx tests/runtime/env-secret-interpolation.run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/cosmos-engine.run.ts && tsx tests/db-runtime/mongodb-engine.run.ts && tsx tests/db-runtime/dynamodb-engine.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.ts && tsx tests/db-runtime/mongodb-integration.opt-in.run.ts && tsx tests/db-runtime/dynamodb-integration.opt-in.run.ts",
|
|
16
16
|
"test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
|
|
17
17
|
"prepublishOnly": "npm run build:dashboard && npm run build"
|
|
18
18
|
},
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@babel/types": "^7.28.5",
|
|
40
40
|
"@cucumber/gherkin": "^37.0.0",
|
|
41
41
|
"@cucumber/messages": "^31.0.0",
|
|
42
|
-
"@sungen/driver-data-factory": "3.2.
|
|
43
|
-
"@sungen/driver-ui": "3.2.
|
|
42
|
+
"@sungen/driver-data-factory": "3.2.15",
|
|
43
|
+
"@sungen/driver-ui": "3.2.15",
|
|
44
44
|
"chalk": "^5.6.2",
|
|
45
45
|
"commander": "^14.0.2",
|
|
46
46
|
"dotenv": "^17.2.3",
|
|
@@ -13,6 +13,7 @@ import { parseSpecFile } from '../../exporters/spec-parser';
|
|
|
13
13
|
import { loadTestData } from '../../exporters/test-data-resolver';
|
|
14
14
|
import { loadSelectorKeyMap } from '../../exporters/selector-key-resolver';
|
|
15
15
|
import { loadPlaywrightReport, resolveResultsPath as resolveResultsPathShared } from '../../exporters/playwright-report-parser';
|
|
16
|
+
import { discoverLocaleVariants } from '../../exporters/locale-variants';
|
|
16
17
|
import { mergeFeatureAndSpec } from '../../exporters/scenario-merger';
|
|
17
18
|
import {
|
|
18
19
|
buildTestCaseRows,
|
|
@@ -167,70 +168,11 @@ function resolveResultsPath(cwd: string, target: DeliveryTarget): string | null
|
|
|
167
168
|
return resolveResultsPathShared(cwd, generatedDir(cwd, target), target.featureBaseName);
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
* `<name>-test-result.<locale>.json` files found in the generated directory.
|
|
176
|
-
*
|
|
177
|
-
* `displayCode` is what we show in sheet names — `BASE_LOCALE.toUpperCase()`
|
|
178
|
-
* for base, the locale code uppercased otherwise (e.g. `EN`, `JA`, `EN-US`).
|
|
179
|
-
*/
|
|
180
|
-
interface LocaleVariant {
|
|
181
|
-
locale: string; // '' for base, 'en' / 'ja' / 'staging-ja' for variants
|
|
182
|
-
displayCode: string; // 'VI' / 'EN' / 'JA' — used in sheet name
|
|
183
|
-
resultsPath: string | null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Base locale assumed when no `SUNGEN_ENV` and no explicit override.
|
|
188
|
-
* Configurable later via `qa/.sungen-config.yaml` or similar — currently
|
|
189
|
-
* hardcoded since every existing sungen project uses Vietnamese as the
|
|
190
|
-
* source-of-truth `test-data/<feature>.yaml`.
|
|
191
|
-
*/
|
|
192
|
-
const DEFAULT_BASE_LOCALE = 'vi';
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Discover every locale variant available for a feature by scanning the
|
|
196
|
-
* generated directory for `<featureBaseName>-test-result*.json` files.
|
|
197
|
-
*
|
|
198
|
-
* Result always includes the base (locale = '') as the first entry — even if
|
|
199
|
-
* `<name>-test-result.json` doesn't exist, callers still need somewhere to
|
|
200
|
-
* fall back to (Pending status). Variants follow alphabetically by locale
|
|
201
|
-
* code, so the sheet order is deterministic across runs.
|
|
202
|
-
*/
|
|
203
|
-
function discoverLocaleVariants(cwd: string, target: DeliveryTarget): LocaleVariant[] {
|
|
204
|
-
const genDir = generatedDir(cwd, target);
|
|
205
|
-
const prefix = `${target.featureBaseName}-test-result`;
|
|
206
|
-
const variants: LocaleVariant[] = [];
|
|
207
|
-
|
|
208
|
-
// Base variant: per-target result file if present, else fall back to the global
|
|
209
|
-
// test-results/results.json (what playwright.config writes by default) via resolveResultsPath.
|
|
210
|
-
variants.push({
|
|
211
|
-
locale: '',
|
|
212
|
-
displayCode: DEFAULT_BASE_LOCALE.toUpperCase(),
|
|
213
|
-
resultsPath: resolveResultsPath(cwd, target),
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
if (fs.existsSync(genDir)) {
|
|
217
|
-
const localeFiles = fs.readdirSync(genDir)
|
|
218
|
-
.filter((f) => f.startsWith(`${prefix}.`) && f.endsWith('.json') && f !== `${prefix}.json`)
|
|
219
|
-
.sort();
|
|
220
|
-
for (const f of localeFiles) {
|
|
221
|
-
// Strip prefix + '.' on the left, '.json' on the right → locale code.
|
|
222
|
-
const locale = f.slice(prefix.length + 1, -'.json'.length);
|
|
223
|
-
if (!locale) continue;
|
|
224
|
-
variants.push({
|
|
225
|
-
locale,
|
|
226
|
-
displayCode: locale.toUpperCase(),
|
|
227
|
-
resultsPath: path.join(genDir, f),
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return variants;
|
|
233
|
-
}
|
|
171
|
+
// Locale/env variant discovery is SHARED with the dashboard snapshot builder —
|
|
172
|
+
// see exporters/locale-variants.ts for the classification rules (declared
|
|
173
|
+
// locales via qa/capabilities.yaml, env pairing, mobile OS exemption). The two
|
|
174
|
+
// surfaces previously hand-mirrored this logic and drifted into the
|
|
175
|
+
// fake-locale bug (any env-suffixed result file became a "locale" sheet).
|
|
234
176
|
|
|
235
177
|
function resolveTestDataPathForTarget(cwd: string, target: DeliveryTarget): string {
|
|
236
178
|
return path.join(qaDir(cwd, target), 'test-data', `${target.featureBaseName}.yaml`);
|
|
@@ -522,7 +464,17 @@ async function exportTarget(
|
|
|
522
464
|
return buildSummary(label, rows, path.relative(cwd, csvPath));
|
|
523
465
|
}
|
|
524
466
|
|
|
525
|
-
const variants = discoverLocaleVariants(
|
|
467
|
+
const { variants, ignored } = discoverLocaleVariants(
|
|
468
|
+
cwd, generatedDir(cwd, target), target.featureBaseName,
|
|
469
|
+
() => resolveResultsPath(cwd, target),
|
|
470
|
+
);
|
|
471
|
+
if (ignored.length > 0) {
|
|
472
|
+
const env = process.env.SUNGEN_ENV;
|
|
473
|
+
log(` ${COLOR.gray}Ignored result file(s) from other environments: ${ignored.join(', ')}${COLOR.reset}`);
|
|
474
|
+
for (const suffix of ignored) {
|
|
475
|
+
log(` ${COLOR.cyan}→ sungen delivery ${target.screen} --env ${suffix}${COLOR.reset}${env ? '' : ` ${COLOR.gray}(this run uses the base results only)${COLOR.reset}`}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
526
478
|
|
|
527
479
|
// Mobile: emit ONE deliverable per OS (csv + xlsx), each from its own
|
|
528
480
|
// `<feature>-test-result.<os>.json`. Android/iOS are SEPARATE platform runs,
|
|
@@ -542,7 +494,11 @@ async function exportTarget(
|
|
|
542
494
|
featureName: feature.featureName,
|
|
543
495
|
featurePath: resolvePlatformAppId(featurePath, variant.locale), // OS-precise app id
|
|
544
496
|
merged,
|
|
545
|
-
|
|
497
|
+
// Overlay = the after-OS tail (what SUNGEN_ENV was during that run):
|
|
498
|
+
// `android.ja` merged <name>.ja.yaml at runtime — the old code passed the
|
|
499
|
+
// full `android.ja` suffix, looked up a nonexistent overlay, and silently
|
|
500
|
+
// showed base-language values in the Test Data column.
|
|
501
|
+
testData: loadTestData(testDataFile, variant.overlay ?? null),
|
|
546
502
|
results: variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null,
|
|
547
503
|
env,
|
|
548
504
|
selectorKeyMap,
|
|
@@ -579,9 +535,11 @@ async function exportTarget(
|
|
|
579
535
|
const multiLocale = variants.length > 1;
|
|
580
536
|
|
|
581
537
|
for (const variant of variants) {
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
|
|
538
|
+
// Overlay parity with the RUNTIME: merge exactly what TestDataLoader merged
|
|
539
|
+
// during that run (the run's SUNGEN_ENV) — the base variant under --env dev
|
|
540
|
+
// merges <feature>.dev.yaml, a ja variant merges <feature>.ja.yaml, plain
|
|
541
|
+
// base merges nothing.
|
|
542
|
+
const variantTestData = loadTestData(testDataFile, variant.overlay ?? null);
|
|
585
543
|
const variantResults = variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null;
|
|
586
544
|
const variantRows = buildTestCaseRows({
|
|
587
545
|
screen: label,
|
|
@@ -713,8 +671,12 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
713
671
|
.argument('[names...]', 'Specific screen or flow names. Omit to process all.')
|
|
714
672
|
.option('--skip-preflight', 'Skip pre-flight checks (not recommended)')
|
|
715
673
|
.option('--continue-on-missing', 'Skip targets with blocking misses instead of aborting')
|
|
716
|
-
.
|
|
674
|
+
.option('--env <env>', 'Environment/locale of the run to deliver (sets SUNGEN_ENV; e.g. dev, staging, ja, dev-ja)')
|
|
675
|
+
.action(async (names: string[], options: { skipPreflight?: boolean; continueOnMissing?: boolean; env?: string }) => {
|
|
717
676
|
try {
|
|
677
|
+
// Same effect as running with SUNGEN_ENV=<env> — result-file resolution,
|
|
678
|
+
// locale pairing, and test-data overlays all key off this one string.
|
|
679
|
+
if (options.env) process.env.SUNGEN_ENV = options.env;
|
|
718
680
|
const cwd = process.cwd();
|
|
719
681
|
|
|
720
682
|
// 1. Scope detection — each positional name expands into one target
|
|
@@ -25,6 +25,7 @@ import { buildScreenSnapshot } from '../exporters/json-exporter';
|
|
|
25
25
|
import { loadApiCatalog } from '../exporters/api-catalog-loader';
|
|
26
26
|
import { buildApiDetailRows } from '../exporters/xlsx-exporter';
|
|
27
27
|
import { resolvePlatformAppId } from '../exporters/mobile-app-id';
|
|
28
|
+
import { discoverLocaleVariants, LocaleVariant } from '../exporters/locale-variants';
|
|
28
29
|
import { getPackageVersion } from '../exporters/package-info';
|
|
29
30
|
import { ApiCatalogEntry, EnvironmentInfo } from '../exporters/types';
|
|
30
31
|
import {
|
|
@@ -38,11 +39,10 @@ import {
|
|
|
38
39
|
SNAPSHOT_VERSION,
|
|
39
40
|
} from './types';
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const DEFAULT_BASE_LOCALE = 'vi';
|
|
42
|
+
// Locale/env variant discovery is SHARED with the delivery CLI — see
|
|
43
|
+
// exporters/locale-variants.ts (declared locales, env pairing, mobile OS
|
|
44
|
+
// exemption). Previously a hand-mirrored copy that drifted into the
|
|
45
|
+
// fake-locale bug.
|
|
46
46
|
|
|
47
47
|
export interface DashboardTarget {
|
|
48
48
|
name: string;
|
|
@@ -217,13 +217,16 @@ function buildOneFeature(
|
|
|
217
217
|
// Discover locale variants by scanning <feature>-test-result*.json files.
|
|
218
218
|
// Always includes base ('') as the first entry, even if its results file
|
|
219
219
|
// is missing — UI still needs to render the feature.
|
|
220
|
-
const variants = discoverLocaleVariants(
|
|
220
|
+
const { variants } = discoverLocaleVariants(
|
|
221
|
+
cwd, genBase, featureBaseName,
|
|
222
|
+
() => resolveResultsPath(cwd, genBase, featureBaseName),
|
|
223
|
+
);
|
|
221
224
|
|
|
222
225
|
// Build one per-variant ScreenSnapshot (test-data overlay + results + OS-precise
|
|
223
226
|
// env column). Reused by both the web/i18n (locale-axis) and mobile (platform-axis) paths.
|
|
224
|
-
const buildVariant = (variant:
|
|
227
|
+
const buildVariant = (variant: LocaleVariant) => {
|
|
225
228
|
const testData = fs.existsSync(testDataFile)
|
|
226
|
-
? loadTestData(testDataFile, variant.
|
|
229
|
+
? loadTestData(testDataFile, variant.overlay ?? null)
|
|
227
230
|
: {};
|
|
228
231
|
const results = variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null;
|
|
229
232
|
return buildScreenSnapshot({
|
|
@@ -341,59 +344,6 @@ function buildOneFeature(
|
|
|
341
344
|
}];
|
|
342
345
|
}
|
|
343
346
|
|
|
344
|
-
/**
|
|
345
|
-
* A single locale variant of a feature.
|
|
346
|
-
* `locale === ''` denotes the base (file `<name>-test-result.json`,
|
|
347
|
-
* test-data without overlay merge). Variants come from
|
|
348
|
-
* `<name>-test-result.<locale>.json` filenames.
|
|
349
|
-
*/
|
|
350
|
-
interface FeatureLocaleVariant {
|
|
351
|
-
locale: string;
|
|
352
|
-
displayCode: string;
|
|
353
|
-
resultsPath: string | null;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Scan the generated directory for `<basename>-test-result*.json` files
|
|
358
|
-
* and infer locale codes. The base entry is always returned first; locale
|
|
359
|
-
* variants follow alphabetically so sheet/tab order stays deterministic.
|
|
360
|
-
*
|
|
361
|
-
* Mirrors `discoverLocaleVariants` in `src/cli/commands/delivery.ts` so the
|
|
362
|
-
* dashboard and the CSV/XLSX delivery agree on what locales exist for a
|
|
363
|
-
* given feature.
|
|
364
|
-
*/
|
|
365
|
-
function discoverLocaleVariants(cwd: string, genDir: string, featureBaseName: string): FeatureLocaleVariant[] {
|
|
366
|
-
const prefix = `${featureBaseName}-test-result`;
|
|
367
|
-
const variants: FeatureLocaleVariant[] = [];
|
|
368
|
-
|
|
369
|
-
// Base variant resolves through the SHARED candidate chain (per-feature file
|
|
370
|
-
// first, then the global test-results/results.json playwright writes by
|
|
371
|
-
// default) — the same resolution delivery.ts uses. Without the fallback the
|
|
372
|
-
// dashboard showed every scenario Pending on projects that only have the
|
|
373
|
-
// global results file, while delivery correctly reported Passed.
|
|
374
|
-
variants.push({
|
|
375
|
-
locale: '',
|
|
376
|
-
displayCode: DEFAULT_BASE_LOCALE.toUpperCase(),
|
|
377
|
-
resultsPath: resolveResultsPath(cwd, genDir, featureBaseName),
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
if (fs.existsSync(genDir)) {
|
|
381
|
-
const localeFiles = fs.readdirSync(genDir)
|
|
382
|
-
.filter((f) => f.startsWith(`${prefix}.`) && f.endsWith('.json') && f !== `${prefix}.json`)
|
|
383
|
-
.sort();
|
|
384
|
-
for (const f of localeFiles) {
|
|
385
|
-
const locale = f.slice(prefix.length + 1, -'.json'.length);
|
|
386
|
-
if (!locale) continue;
|
|
387
|
-
variants.push({
|
|
388
|
-
locale,
|
|
389
|
-
displayCode: locale.toUpperCase(),
|
|
390
|
-
resultsPath: path.join(genDir, f),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return variants;
|
|
396
|
-
}
|
|
397
347
|
|
|
398
348
|
/**
|
|
399
349
|
* Combine per-feature summaries into a single screen-level rollup.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale/env classification for `<feature>-test-result.<suffix>.json` files —
|
|
3
|
+
* SHARED by the delivery CLI and the dashboard snapshot builder (previously two
|
|
4
|
+
* hand-mirrored copies that had already drifted into the fake-locale bug).
|
|
5
|
+
*
|
|
6
|
+
* Field report (3.2.14): every result-file suffix was treated as a LOCALE. A
|
|
7
|
+
* team running `SUNGEN_ENV=dev npx playwright test` (the standard .env.qa.dev
|
|
8
|
+
* convention) then `sungen delivery` without an env got a fabricated two-locale
|
|
9
|
+
* deliverable: a "VI Auto" sheet (hardcoded base label, all Pending, pointing
|
|
10
|
+
* at a result file that doesn't exist) next to a "DEV Auto" sheet.
|
|
11
|
+
*
|
|
12
|
+
* The file mechanism cannot distinguish an env overlay (`login.dev.yaml`,
|
|
13
|
+
* per-env credentials) from a locale overlay (`login.ja.yaml`, translated
|
|
14
|
+
* text) — they are deliberately the same mechanism keyed by SUNGEN_ENV. So
|
|
15
|
+
* locales are DECLARED, not guessed:
|
|
16
|
+
*
|
|
17
|
+
* # qa/capabilities.yaml
|
|
18
|
+
* locales: [ja, en] # non-base locales (written by /sungen:locale)
|
|
19
|
+
* base_locale: en # language of the base test-data (default: en)
|
|
20
|
+
*
|
|
21
|
+
* Suffix classification (first match wins), with E = the delivery env
|
|
22
|
+
* (SUNGEN_ENV / --env, possibly empty):
|
|
23
|
+
* 1. ^(android|ios)(.|$) → OS run (mobile per-OS branch consumes)
|
|
24
|
+
* 2. suffix === E → the base variant's own file (not a variant)
|
|
25
|
+
* 3. E === '' && suffix ∈ locales → locale variant on the base env
|
|
26
|
+
* 4. E !== '' && suffix === `${E}-<L>`, L ∈ locales → locale variant on env E
|
|
27
|
+
* 5. anything else → env run — EXCLUDED from variants and
|
|
28
|
+
* reported in `ignored` so the CLI can hint "run with --env <suffix>".
|
|
29
|
+
*
|
|
30
|
+
* Env pairing (rules 2–4) means a delivery only ever mixes results from ONE
|
|
31
|
+
* environment. Running with `--env ja` (an env named exactly like a declared
|
|
32
|
+
* locale) is interpreted as "deliver that locale's run" — the documented
|
|
33
|
+
* SUNGEN_ENV=<locale> convention, not a collision.
|
|
34
|
+
*/
|
|
35
|
+
import * as fs from 'fs';
|
|
36
|
+
import * as path from 'path';
|
|
37
|
+
import { parse as parseYaml } from 'yaml';
|
|
38
|
+
|
|
39
|
+
export interface LocaleVariant {
|
|
40
|
+
/** Result-file suffix as found on disk ('' = base). */
|
|
41
|
+
locale: string;
|
|
42
|
+
/** Sheet-name label (used only in multi-locale mode). */
|
|
43
|
+
displayCode: string;
|
|
44
|
+
resultsPath: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Test-data overlay code to merge for this variant's Test Data column —
|
|
47
|
+
* mirrors what the RUNTIME merged (TestDataLoader uses SUNGEN_ENV): the full
|
|
48
|
+
* suffix on web, the after-OS tail on mobile (`android.ja` ran with
|
|
49
|
+
* SUNGEN_ENV=ja). `null` = base data only. loadTestData no-ops on a missing
|
|
50
|
+
* overlay file, so passing an env code whose overlay doesn't exist is safe.
|
|
51
|
+
*/
|
|
52
|
+
overlay: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LocaleDiscovery {
|
|
56
|
+
variants: LocaleVariant[];
|
|
57
|
+
/** Env-run suffixes with result files, excluded from this delivery (hint material). */
|
|
58
|
+
ignored: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LocaleConfig {
|
|
62
|
+
baseLocale: string;
|
|
63
|
+
locales: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const DEFAULT_BASE_LOCALE = 'en';
|
|
67
|
+
|
|
68
|
+
/** Read the i18n declaration from qa/capabilities.yaml (absent → no locales, base 'en'). */
|
|
69
|
+
export function readLocaleConfig(cwd: string): LocaleConfig {
|
|
70
|
+
try {
|
|
71
|
+
const p = path.join(cwd, 'qa', 'capabilities.yaml');
|
|
72
|
+
if (fs.existsSync(p)) {
|
|
73
|
+
const parsed = parseYaml(fs.readFileSync(p, 'utf-8')) as
|
|
74
|
+
{ locales?: unknown; base_locale?: unknown } | null;
|
|
75
|
+
const locales = Array.isArray(parsed?.locales)
|
|
76
|
+
? parsed!.locales!.filter((l): l is string => typeof l === 'string' && l.length > 0)
|
|
77
|
+
: [];
|
|
78
|
+
const baseLocale = typeof parsed?.base_locale === 'string' && parsed.base_locale
|
|
79
|
+
? parsed.base_locale
|
|
80
|
+
: DEFAULT_BASE_LOCALE;
|
|
81
|
+
return { baseLocale, locales };
|
|
82
|
+
}
|
|
83
|
+
} catch { /* malformed yaml → behave as undeclared */ }
|
|
84
|
+
return { baseLocale: DEFAULT_BASE_LOCALE, locales: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const OS_SUFFIX = /^(android|ios)(\.|$)/;
|
|
88
|
+
|
|
89
|
+
/** The after-OS tail of a mobile suffix: 'android.ja' → 'ja', 'ios' → ''. */
|
|
90
|
+
export function osSuffixTail(suffix: string): string {
|
|
91
|
+
return suffix.replace(OS_SUFFIX, '');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Discover the variants of a feature for ONE delivery run.
|
|
96
|
+
*
|
|
97
|
+
* `resolveBaseResultsPath` supplies the base variant's results (delivery and
|
|
98
|
+
* the dashboard share the same env-aware candidate chain via
|
|
99
|
+
* playwright-report-parser.resolveResultsPath — passed in to avoid a cycle).
|
|
100
|
+
*/
|
|
101
|
+
export function discoverLocaleVariants(
|
|
102
|
+
cwd: string,
|
|
103
|
+
genDir: string,
|
|
104
|
+
featureBaseName: string,
|
|
105
|
+
resolveBaseResultsPath: () => string | null,
|
|
106
|
+
): LocaleDiscovery {
|
|
107
|
+
const cfg = readLocaleConfig(cwd);
|
|
108
|
+
const env = process.env.SUNGEN_ENV || '';
|
|
109
|
+
const prefix = `${featureBaseName}-test-result`;
|
|
110
|
+
|
|
111
|
+
const variants: LocaleVariant[] = [{
|
|
112
|
+
locale: '',
|
|
113
|
+
// With --env <locale> (the SUNGEN_ENV=<locale> convention) the base IS that
|
|
114
|
+
// locale's run; otherwise the base carries the declared base language.
|
|
115
|
+
displayCode: (cfg.locales.includes(env) ? env : cfg.baseLocale).toUpperCase(),
|
|
116
|
+
resultsPath: resolveBaseResultsPath(),
|
|
117
|
+
overlay: env || null,
|
|
118
|
+
}];
|
|
119
|
+
const ignored: string[] = [];
|
|
120
|
+
|
|
121
|
+
if (fs.existsSync(genDir)) {
|
|
122
|
+
const suffixes = fs.readdirSync(genDir)
|
|
123
|
+
.filter((f) => f.startsWith(`${prefix}.`) && f.endsWith('.json') && f !== `${prefix}.json`)
|
|
124
|
+
.map((f) => f.slice(prefix.length + 1, -'.json'.length))
|
|
125
|
+
.filter((s) => s.length > 0)
|
|
126
|
+
.sort();
|
|
127
|
+
for (const suffix of suffixes) {
|
|
128
|
+
if (OS_SUFFIX.test(suffix)) {
|
|
129
|
+
// Mobile per-OS run — kept unconditionally; the mobile branch consumes these.
|
|
130
|
+
// Overlay = the tail (what SUNGEN_ENV was during that run), never the OS itself.
|
|
131
|
+
variants.push({
|
|
132
|
+
locale: suffix,
|
|
133
|
+
displayCode: suffix.toUpperCase(),
|
|
134
|
+
resultsPath: path.join(genDir, `${prefix}.${suffix}.json`),
|
|
135
|
+
overlay: osSuffixTail(suffix) || null,
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (suffix === env) continue; // the base variant's own file
|
|
140
|
+
const isLocaleOfThisEnv = env === ''
|
|
141
|
+
? cfg.locales.includes(suffix)
|
|
142
|
+
: suffix.startsWith(`${env}-`) && cfg.locales.includes(suffix.slice(env.length + 1));
|
|
143
|
+
if (isLocaleOfThisEnv) {
|
|
144
|
+
variants.push({
|
|
145
|
+
locale: suffix,
|
|
146
|
+
displayCode: suffix.toUpperCase(),
|
|
147
|
+
resultsPath: path.join(genDir, `${prefix}.${suffix}.json`),
|
|
148
|
+
overlay: suffix,
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
ignored.push(suffix);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { variants, ignored };
|
|
157
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{{#if selectedProbe}}
|
|
2
|
+
{ const el = {{> locator-base}}.filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}};
|
|
3
|
+
if ((await el.getAttribute('aria-selected')) !== null) {
|
|
4
|
+
await expect(el).toHaveAttribute('aria-selected', 'true');
|
|
5
|
+
} else {
|
|
6
|
+
await expect(el).toBeChecked();
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
{{else}}
|
|
10
|
+
await expect({{> locator-base}}.filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).{{{stateAssertion}}};
|
|
11
|
+
{{/if}}
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
{{~#if isGiven~}}
|
|
2
1
|
{
|
|
3
|
-
const
|
|
2
|
+
const table = {{> locator}};
|
|
3
|
+
const rows = table.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true });
|
|
4
4
|
{{#each assertions}}
|
|
5
|
-
{{this}}
|
|
5
|
+
{{{this}}}
|
|
6
6
|
{{/each~}}
|
|
7
7
|
}
|
|
8
|
-
{{~else~}}
|
|
9
|
-
{
|
|
10
|
-
const rows = {{> locator}}.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true });
|
|
11
|
-
{{#each assertions}}
|
|
12
|
-
{{this}}
|
|
13
|
-
{{/each~}}
|
|
14
|
-
}
|
|
15
|
-
{{~/if}}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
{{~#if rowSelector}}
|
|
2
|
+
{{~#if isGiven}}
|
|
3
|
+
const tableRow = {{> locator}}.locator('{{rowSelector}}');
|
|
4
|
+
await tableRow.filter({ hasText: '{{escapeQuotes filterValue}}' }).waitFor();
|
|
5
|
+
{{~else}}
|
|
6
|
+
const tableRow = {{> locator}}.locator('{{rowSelector}}');
|
|
7
|
+
await expect(tableRow).toContainText('{{escapeQuotes filterValue}}');
|
|
8
|
+
{{~/if}}
|
|
9
|
+
{{~else}}
|
|
1
10
|
{{~#if isGiven}}
|
|
2
11
|
const tableRow = {{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
|
|
3
12
|
await tableRow.waitFor();
|
|
4
13
|
{{~else}}
|
|
5
14
|
const tableRow = {{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
|
|
6
15
|
await expect(tableRow).toBeVisible();
|
|
7
|
-
{{~/if}}
|
|
16
|
+
{{~/if}}
|
|
17
|
+
{{~/if}}
|
|
@@ -908,7 +908,7 @@ export class CodeGenerator {
|
|
|
908
908
|
// them. Rewrite those to the per-row `rowData` view — each row then fires its own `@api` call
|
|
909
909
|
// with that row's input and asserts that row's expected status/body (the success/failure matrix).
|
|
910
910
|
// `testData.cases()/withRow()` (the global loader, no `.get/.bind/.set`) is intentionally left alone.
|
|
911
|
-
return transformToRuntimeData(rendered, 'rowData').replace(/\btestData\.(get|bind|set)\(/g, 'rowData.$1(');
|
|
911
|
+
return transformToRuntimeData(rendered, 'rowData').replace(/\btestData\.(get|bind|set|raw)\(/g, 'rowData.$1(');
|
|
912
912
|
}
|
|
913
913
|
return rendered;
|
|
914
914
|
}
|
|
@@ -937,6 +937,40 @@ export class CodeGenerator {
|
|
|
937
937
|
/**
|
|
938
938
|
* Generate tests for all features in a directory
|
|
939
939
|
*/
|
|
940
|
+
/**
|
|
941
|
+
* SG-W009 — every `${QA_*}` secret reference in the unit's test-data yaml must be
|
|
942
|
+
* DECLARED: as a real value in .env.qa (or an overlay .env.qa.<env>), or at least as
|
|
943
|
+
* a name in the committed .env.qa.example. An undeclared ref means the test throws
|
|
944
|
+
* "environment variable not set" the moment the key is used — surface it at compile
|
|
945
|
+
* time instead. Only the `${QA_*}` shape is checked: any other `${...}` text is
|
|
946
|
+
* ordinary literal data by design (never resolved, never warned about).
|
|
947
|
+
*/
|
|
948
|
+
private reportedEnvRefs = new Set<string>();
|
|
949
|
+
private checkEnvSecretRefs(featureFile: string): void {
|
|
950
|
+
const testDataDir = path.join(path.dirname(featureFile), '..', 'test-data');
|
|
951
|
+
if (!fs.existsSync(testDataDir)) return;
|
|
952
|
+
const referenced = new Set<string>();
|
|
953
|
+
for (const f of fs.readdirSync(testDataDir).filter((x) => x.endsWith('.yaml') || x.endsWith('.yml'))) {
|
|
954
|
+
const text = fs.readFileSync(path.join(testDataDir, f), 'utf-8');
|
|
955
|
+
for (const m of text.matchAll(/\$\{(QA_[A-Z0-9_]+)\}/g)) referenced.add(m[1]);
|
|
956
|
+
}
|
|
957
|
+
if (!referenced.size) return;
|
|
958
|
+
const declared = new Set<string>();
|
|
959
|
+
for (const f of fs.readdirSync(process.cwd()).filter((x) => x === '.env.qa' || x.startsWith('.env.qa.'))) {
|
|
960
|
+
for (const line of fs.readFileSync(path.join(process.cwd(), f), 'utf-8').split('\n')) {
|
|
961
|
+
const m = line.match(/^\s*#?\s*(QA_[A-Z0-9_]+)\s*=/);
|
|
962
|
+
if (m) declared.add(m[1]);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
for (const name of referenced) {
|
|
966
|
+
if (declared.has(name) || this.reportedEnvRefs.has(name)) continue;
|
|
967
|
+
this.reportedEnvRefs.add(name);
|
|
968
|
+
this.diagnostics.add('SG-W009',
|
|
969
|
+
`test-data references \${${name}} but ${name} is declared in neither .env.qa nor .env.qa.example — the test will throw "environment variable not set" when the key is used. Add ${name}=<value> to .env.qa (gitignored), and list the name in .env.qa.example`,
|
|
970
|
+
{ feature: path.basename(featureFile, '.feature'), step: name });
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
940
974
|
async generateAllTests(
|
|
941
975
|
featuresDir: string,
|
|
942
976
|
outputDir: string,
|
|
@@ -956,6 +990,7 @@ export class CodeGenerator {
|
|
|
956
990
|
try {
|
|
957
991
|
const feature = parser.parseFeatureFile(featureFile);
|
|
958
992
|
features.push(feature);
|
|
993
|
+
this.checkEnvSecretRefs(featureFile);
|
|
959
994
|
} catch (error) {
|
|
960
995
|
console.error(`✗ Failed to parse ${featureFile}:`, error);
|
|
961
996
|
parseErrors.push({ file: featureFile, error });
|
|
@@ -15,12 +15,17 @@
|
|
|
15
15
|
* Then (invalid Gherkin; comments cannot substitute), or a documentation-style
|
|
16
16
|
* manual (no steps) whose comments lack the extractable procedure — ≥1 numbered
|
|
17
17
|
* action line + ≥1 labeled Observable:/Oracle:/Expected: line
|
|
18
|
+
* SG-W009 — test-data references a `${QA_*}` secret that is declared in neither .env.qa
|
|
19
|
+
* nor .env.qa.example — the test will throw "not set" the moment the key is used
|
|
20
|
+
* SG-W010 — a row step's [Ref] looks POSITIONAL (First/Last/Top/…) but has no selectors.yaml
|
|
21
|
+
* entry: the label is display-only, so the step checks existence anywhere in the
|
|
22
|
+
* table — an ordering rule would pass even when the row moved
|
|
18
23
|
*
|
|
19
24
|
* Diagnostics never block generation by themselves; `sungen generate --strict`
|
|
20
25
|
* turns any collected diagnostic into a non-zero exit.
|
|
21
26
|
*/
|
|
22
27
|
|
|
23
|
-
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008';
|
|
28
|
+
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010';
|
|
24
29
|
|
|
25
30
|
export interface Diagnostic {
|
|
26
31
|
code: DiagnosticCode;
|