@sun-asterisk/sungen 3.2.32 → 3.2.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/audit.d.ts +1 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +10 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +35 -17
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +39 -18
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +14 -7
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts.map +1 -1
- package/dist/cli/commands/results-merge.js +2 -12
- package/dist/cli/commands/results-merge.js.map +1 -1
- package/dist/cli/commands/results-sanitize.d.ts.map +1 -1
- package/dist/cli/commands/results-sanitize.js +2 -13
- package/dist/cli/commands/results-sanitize.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +6 -10
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +21 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +32 -1
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/locale-variants.d.ts +7 -1
- package/dist/exporters/locale-variants.d.ts.map +1 -1
- package/dist/exporters/locale-variants.js +23 -16
- package/dist/exporters/locale-variants.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +13 -7
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/carry-manual-results.d.ts +70 -0
- package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -0
- package/dist/exporters/matrix/carry-manual-results.js +232 -0
- package/dist/exporters/matrix/carry-manual-results.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +2 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +7 -1
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/wording.d.ts +7 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -1
- package/dist/exporters/matrix/wording.js +27 -1
- package/dist/exporters/matrix/wording.js.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +2 -0
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +7 -2
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
- package/dist/generators/test-generator/code-generator.d.ts +2 -1
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +67 -3
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +33 -4
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +32 -3
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +12 -2
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +15 -9
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +15 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts +5 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/with-parent-scope.js +6 -0
- package/dist/generators/test-generator/patterns/with-parent-scope.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +46 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +84 -9
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +25 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +39 -8
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-entry-fields.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-entry-fields.js +1 -0
- package/dist/generators/test-generator/utils/selector-entry-fields.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +37 -4
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +212 -79
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +12 -4
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability.d.ts +7 -1
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +15 -12
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +5 -5
- package/dist/harness/config-file.d.ts +17 -0
- package/dist/harness/config-file.d.ts.map +1 -0
- package/dist/harness/config-file.js +100 -0
- package/dist/harness/config-file.js.map +1 -0
- package/dist/harness/depth-lint.d.ts.map +1 -1
- package/dist/harness/depth-lint.js +2 -1
- package/dist/harness/depth-lint.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +6 -2
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/keyword-match.d.ts +59 -0
- package/dist/harness/keyword-match.d.ts.map +1 -0
- package/dist/harness/keyword-match.js +86 -0
- package/dist/harness/keyword-match.js.map +1 -0
- package/dist/harness/perf.d.ts +7 -1
- package/dist/harness/perf.d.ts.map +1 -1
- package/dist/harness/perf.js +13 -10
- package/dist/harness/perf.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +9 -2
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +16 -4
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -1
- package/dist/harness/read-text.d.ts +6 -0
- package/dist/harness/read-text.d.ts.map +1 -1
- package/dist/harness/read-text.js +7 -1
- package/dist/harness/read-text.js.map +1 -1
- package/dist/harness/secret-scan.d.ts +27 -1
- package/dist/harness/secret-scan.d.ts.map +1 -1
- package/dist/harness/secret-scan.js +57 -4
- package/dist/harness/secret-scan.js.map +1 -1
- package/dist/harness/sensors.d.ts +12 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +41 -18
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-branches.d.ts +0 -18
- package/dist/harness/spec-branches.d.ts.map +1 -1
- package/dist/harness/spec-branches.js +23 -2
- package/dist/harness/spec-branches.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +14 -4
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +2 -1
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/harness/write-atomic.d.ts +2 -0
- package/dist/harness/write-atomic.d.ts.map +1 -0
- package/dist/harness/write-atomic.js +62 -0
- package/dist/harness/write-atomic.js.map +1 -0
- package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
- package/dist/orchestrator/ai-skills/writer.js +15 -7
- package/dist/orchestrator/ai-skills/writer.js.map +1 -1
- package/dist/orchestrator/assets-drift.d.ts.map +1 -1
- package/dist/orchestrator/assets-drift.js +11 -6
- package/dist/orchestrator/assets-drift.js.map +1 -1
- package/dist/orchestrator/project-deps-sync.d.ts.map +1 -1
- package/dist/orchestrator/project-deps-sync.js +22 -1
- package/dist/orchestrator/project-deps-sync.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +8 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +49 -7
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/security-overrides-catalog.d.ts +39 -0
- package/dist/orchestrator/security-overrides-catalog.d.ts.map +1 -0
- package/dist/orchestrator/security-overrides-catalog.js +63 -0
- package/dist/orchestrator/security-overrides-catalog.js.map +1 -0
- package/dist/orchestrator/security-overrides.d.ts +51 -0
- package/dist/orchestrator/security-overrides.d.ts.map +1 -0
- package/dist/orchestrator/security-overrides.js +165 -0
- package/dist/orchestrator/security-overrides.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +3 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +9 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/dist/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/dist/orchestrator/templates/specs-url-assert.d.ts +17 -1
- package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.js +14 -2
- package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.ts +24 -2
- package/dist/orchestrator/templates/wdio.conf.ts +24 -1
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +10 -2
- package/src/cli/commands/capability.ts +51 -18
- package/src/cli/commands/delivery.ts +38 -16
- package/src/cli/commands/generate.ts +15 -7
- package/src/cli/commands/results-merge.ts +2 -8
- package/src/cli/commands/results-sanitize.ts +2 -9
- package/src/dashboard/snapshot-builder.ts +6 -8
- package/src/exporters/feature-parser.ts +32 -2
- package/src/exporters/locale-variants.ts +23 -17
- package/src/exporters/matrix/build.ts +14 -8
- package/src/exporters/matrix/carry-manual-results.ts +229 -0
- package/src/exporters/matrix/export.ts +10 -4
- package/src/exporters/matrix/wording.ts +26 -1
- package/src/exporters/xlsx-exporter.ts +7 -2
- package/src/generators/test-generator/adapters/adapter-interface.ts +2 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/reinstall-app-action.hbs +2 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +8 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-with-role-variable-assertion.hbs +7 -3
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +68 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +7 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +8 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +5 -1
- package/src/generators/test-generator/code-generator.ts +54 -3
- package/src/generators/test-generator/diagnostics.ts +33 -4
- package/src/generators/test-generator/patterns/index.ts +15 -8
- package/src/generators/test-generator/patterns/types.ts +15 -0
- package/src/generators/test-generator/patterns/with-parent-scope.ts +6 -0
- package/src/generators/test-generator/step-mapper.ts +90 -8
- package/src/generators/test-generator/utils/data-resolver.ts +41 -12
- package/src/generators/test-generator/utils/selector-entry-fields.ts +1 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +238 -82
- package/src/harness/audit.ts +17 -7
- package/src/harness/capability.ts +23 -12
- package/src/harness/catalog/universal-viewpoints.yaml +5 -5
- package/src/harness/config-file.ts +66 -0
- package/src/harness/depth-lint.ts +2 -1
- package/src/harness/flow-check.ts +6 -2
- package/src/harness/keyword-match.ts +85 -0
- package/src/harness/perf.ts +15 -7
- package/src/harness/quality-gates.ts +16 -4
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/read-text.ts +7 -1
- package/src/harness/secret-scan.ts +60 -4
- package/src/harness/sensors.ts +50 -19
- package/src/harness/spec-branches.ts +5 -2
- package/src/harness/spec-coverage.ts +14 -4
- package/src/harness/viewpoint-ledger.ts +2 -1
- package/src/harness/write-atomic.ts +22 -0
- package/src/orchestrator/ai-skills/writer.ts +18 -3
- package/src/orchestrator/assets-drift.ts +10 -5
- package/src/orchestrator/project-deps-sync.ts +23 -1
- package/src/orchestrator/project-initializer.ts +54 -6
- package/src/orchestrator/security-overrides-catalog.ts +69 -0
- package/src/orchestrator/security-overrides.ts +149 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +3 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +9 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +13 -1
- package/src/orchestrator/templates/specs-locale-fixture.ts +21 -9
- package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/src/orchestrator/templates/specs-url-assert.ts +24 -2
- package/src/orchestrator/templates/wdio.conf.ts +24 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { getPackageVersion } from '../exporters/package-info';
|
|
4
|
+
import { readJsonConfig, warnUnreadableConfig } from '../harness/config-file';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Are this project's AI assets (commands + skills under `.claude/` etc.) the ones
|
|
@@ -27,12 +28,16 @@ export function checkAssetsDrift(cwd: string): AssetsDrift | null {
|
|
|
27
28
|
const manifestPath = path.join(cwd, '.sungen', 'manifest.json');
|
|
28
29
|
if (!fs.existsSync(manifestPath)) return null; // not a sungen project (yet)
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const read = readJsonConfig<{ version?: string; managed?: Record<string, string> }>(manifestPath);
|
|
32
|
+
if (read.state === 'unreadable') {
|
|
33
|
+
// This function IS the drift alarm. Staying silent on a broken manifest disables the only
|
|
34
|
+
// warning that a project's .claude/ assets have gone stale — and a truncated manifest is the
|
|
35
|
+
// likeliest way to get one (it is written non-atomically by `ai-skills/writer.ts`).
|
|
36
|
+
warnUnreadableConfig(manifestPath, read.error, 'stale-assets drift cannot be checked — re-run `sungen init` to rewrite it');
|
|
37
|
+
return null;
|
|
35
38
|
}
|
|
39
|
+
if (read.state === 'absent') return null;
|
|
40
|
+
const manifest = read.value;
|
|
36
41
|
const recordedVersion = manifest.version ?? '';
|
|
37
42
|
const runningVersion = getPackageVersion();
|
|
38
43
|
const managed = manifest.managed ?? {};
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from 'child_process';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { resolveDriverVersion } from './lockstep-version';
|
|
5
|
+
import { ensureSecurityOverrides, reportSecurityOverrides } from './security-overrides';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Project-local sync of the sungen package family after a global `sungen update`.
|
|
@@ -143,8 +144,29 @@ export function syncProjectSungenDeps(cwd: string, core: string, dryRun: boolean
|
|
|
143
144
|
}
|
|
144
145
|
for (const s of plan.skipped) console.log(` – skipped ${s.pkg} — ${s.why}`);
|
|
145
146
|
|
|
147
|
+
// A project scaffolded before #764 carries no security overrides, and npm will never pick them up
|
|
148
|
+
// from sungen's own manifest — `sungen update` is the one pass that can put them there. Write them
|
|
149
|
+
// before any install below, since npm reads `overrides` when it resolves.
|
|
150
|
+
const overridePlan = ensureSecurityOverrides(path.join(cwd, 'package.json'), dryRun);
|
|
151
|
+
reportSecurityOverrides(overridePlan, dryRun);
|
|
152
|
+
|
|
146
153
|
if (!plan.specs.length) {
|
|
147
|
-
console.log(` ✓ already on ${core}: ${plan.found.map((f) => f.pkg).join(', ')}
|
|
154
|
+
console.log(` ✓ already on ${core}: ${plan.found.map((f) => f.pkg).join(', ')}`);
|
|
155
|
+
// Family already in lockstep, so nothing would re-resolve — but freshly written overrides only
|
|
156
|
+
// reach the tree through a resolution pass. Run one.
|
|
157
|
+
if (overridePlan?.add.length) {
|
|
158
|
+
if (dryRun) {
|
|
159
|
+
console.log(' [dry-run] would run: npm install (re-resolve under the new security overrides)\n');
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
console.log(' 📦 npm install (re-resolving under the new security overrides)');
|
|
163
|
+
const r = spawnSync('npm', ['install'], { cwd, stdio: 'inherit', shell: true });
|
|
164
|
+
if (r.status !== 0) {
|
|
165
|
+
console.warn('⚠️ Overrides were written to package.json but `npm install` failed — run it by hand.\n');
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
console.log('');
|
|
148
170
|
return true;
|
|
149
171
|
}
|
|
150
172
|
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
installMobileDriver,
|
|
18
18
|
} from './mobile-runtime-scaffolder';
|
|
19
19
|
import { emitAppiumEnvironment, runAppiumEnvironmentSetup, appiumEnvironmentRerunCommand } from './appium-environment-setup';
|
|
20
|
+
import { ensureSecurityOverrides, reportSecurityOverrides } from './security-overrides';
|
|
21
|
+
import { readTextFile } from '../harness/read-text';
|
|
20
22
|
|
|
21
23
|
export { MobileInitOptions };
|
|
22
24
|
|
|
@@ -94,6 +96,14 @@ export class ProjectInitializer {
|
|
|
94
96
|
private cwd: string;
|
|
95
97
|
private createdItems: string[] = [];
|
|
96
98
|
private skippedItems: string[] = [];
|
|
99
|
+
/**
|
|
100
|
+
* Actions the user asked for that could NOT be performed. Kept apart from `skippedItems`:
|
|
101
|
+
* that list renders under "Already exists", which is a statement of fact about the project —
|
|
102
|
+
* printing "package.json scripts (invalid JSON)" under it tells the user the opposite of what
|
|
103
|
+
* happened. An action that was not done needs its own heading, or the init summary reads as a
|
|
104
|
+
* clean success while `npm test` is missing.
|
|
105
|
+
*/
|
|
106
|
+
private notDoneItems: string[] = [];
|
|
97
107
|
/** set by createCodexMCPConfig when the project isn't Codex-trusted (config would never load) */
|
|
98
108
|
private codexTrustWarning = false;
|
|
99
109
|
private agents: AdapterKey[];
|
|
@@ -135,6 +145,7 @@ export class ProjectInitializer {
|
|
|
135
145
|
: this.baseCwd;
|
|
136
146
|
this.createdItems = [];
|
|
137
147
|
this.skippedItems = [];
|
|
148
|
+
this.notDoneItems = [];
|
|
138
149
|
|
|
139
150
|
console.log(`🚀 Initializing Sungen ${this.mobileOnly ? 'mobile-only ' : ''}project...\n`);
|
|
140
151
|
|
|
@@ -439,9 +450,17 @@ export class ProjectInitializer {
|
|
|
439
450
|
|
|
440
451
|
if (existed) {
|
|
441
452
|
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
453
|
+
// via readTextFile: a UTF-8 BOM is the one thing `JSON.parse` rejects that every other tool
|
|
454
|
+
// reading this file ignores, and it is invisible in an editor. A config the rest of the
|
|
455
|
+
// toolchain loads fine must not cost the user their MCP servers here.
|
|
456
|
+
config = JSON.parse(readTextFile(filePath));
|
|
457
|
+
} catch (e) {
|
|
458
|
+
// Anything still unparseable is genuinely broken. Say WHERE — "is not valid JSON" alone
|
|
459
|
+
// leaves the user to find a missing comma in a file they did not know was the problem.
|
|
460
|
+
this.notDoneItems.push(
|
|
461
|
+
`${displayName} (${(e as Error).message.split('\n')[0]}) — MCP servers not registered; `
|
|
462
|
+
+ `fix the JSON and re-run \`sungen init\``,
|
|
463
|
+
);
|
|
445
464
|
return;
|
|
446
465
|
}
|
|
447
466
|
}
|
|
@@ -487,6 +506,13 @@ export class ProjectInitializer {
|
|
|
487
506
|
});
|
|
488
507
|
}
|
|
489
508
|
|
|
509
|
+
if (this.notDoneItems.length > 0) {
|
|
510
|
+
console.log('\n⚠ Not done — needs your attention:');
|
|
511
|
+
this.notDoneItems.forEach(item => {
|
|
512
|
+
console.log(` ✗ ${item}`);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
490
516
|
if (this.codexTrustWarning) {
|
|
491
517
|
console.log('\n⚠ Codex: this project is not trusted yet, so .codex/config.toml (the MCP browser');
|
|
492
518
|
console.log(' config for run-test) will be SILENTLY IGNORED by Codex. To fix, either:');
|
|
@@ -685,6 +711,13 @@ export class ProjectInitializer {
|
|
|
685
711
|
// Ensure standard scripts exist in package.json
|
|
686
712
|
this.ensurePackageScripts(packageJsonPath);
|
|
687
713
|
|
|
714
|
+
// Pin the transitive packages that would otherwise make `npm audit` red here. This MUST happen
|
|
715
|
+
// before the install below: npm reads `overrides` at resolution time, so a lockfile written
|
|
716
|
+
// first would already hold the vulnerable versions. See security-overrides.ts for why the pins
|
|
717
|
+
// cannot simply live in sungen's own manifest.
|
|
718
|
+
const overridePlan = ensureSecurityOverrides(packageJsonPath);
|
|
719
|
+
reportSecurityOverrides(overridePlan);
|
|
720
|
+
|
|
688
721
|
// Mobile-only drops @playwright/test (no web runner needed). `ajv`/`ajv-formats` back the API
|
|
689
722
|
// Driver's `expect {{name.body}} matches schema [Ref]` (loaded lazily; harmless if unused).
|
|
690
723
|
// fast-xml-parser: the API driver's XML request/response support lazy-requires it
|
|
@@ -696,7 +729,7 @@ export class ProjectInitializer {
|
|
|
696
729
|
|
|
697
730
|
let missingDeps: string[] = requiredDeps;
|
|
698
731
|
try {
|
|
699
|
-
const packageJson = JSON.parse(
|
|
732
|
+
const packageJson = JSON.parse(readTextFile(packageJsonPath));
|
|
700
733
|
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
701
734
|
missingDeps = requiredDeps.filter(d => !deps[d]);
|
|
702
735
|
} catch {
|
|
@@ -705,6 +738,12 @@ export class ProjectInitializer {
|
|
|
705
738
|
|
|
706
739
|
if (missingDeps.length === 0) {
|
|
707
740
|
console.log('✓ All dependencies already installed\n');
|
|
741
|
+
// Nothing to install means nothing re-resolves, so overrides just written would sit in
|
|
742
|
+
// package.json while the lockfile keeps the vulnerable tree. Re-resolve once so they land.
|
|
743
|
+
if (overridePlan?.add.length) {
|
|
744
|
+
console.log('📦 Re-resolving the tree so the new security overrides take effect...\n');
|
|
745
|
+
execSync('npm install', execOpts);
|
|
746
|
+
}
|
|
708
747
|
} else {
|
|
709
748
|
console.log(`📦 Installing ${missingDeps.join(', ')}...\n`);
|
|
710
749
|
execSync(`npm install -D ${missingDeps.join(' ')}`, execOpts);
|
|
@@ -725,8 +764,17 @@ export class ProjectInitializer {
|
|
|
725
764
|
private ensurePackageScripts(packageJsonPath: string): void {
|
|
726
765
|
let packageJson: Record<string, any>;
|
|
727
766
|
try {
|
|
728
|
-
|
|
729
|
-
|
|
767
|
+
// via readTextFile: a package.json carrying a UTF-8 BOM is one npm reads without complaint,
|
|
768
|
+
// so it must not be treated as unparseable here. That was the trigger that actually reached
|
|
769
|
+
// this catch — genuinely broken JSON aborts `npm install` a few lines below instead.
|
|
770
|
+
packageJson = JSON.parse(readTextFile(packageJsonPath));
|
|
771
|
+
} catch (e) {
|
|
772
|
+
// Returning here used to add nothing and say nothing, so init reported success and the user
|
|
773
|
+
// met `npm error Missing script: "test"` later with nothing pointing back to this run.
|
|
774
|
+
this.notDoneItems.push(
|
|
775
|
+
`package.json scripts (${(e as Error).message.split('\n')[0]}) — `
|
|
776
|
+
+ `fix the JSON and re-run \`sungen init\`, or add the scripts by hand`,
|
|
777
|
+
);
|
|
730
778
|
return;
|
|
731
779
|
}
|
|
732
780
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm `overrides` a sungen project needs in order to pass `npm audit`.
|
|
3
|
+
*
|
|
4
|
+
* Why this lives in the CLI instead of in `packages/core/package.json`: npm honours `overrides`
|
|
5
|
+
* ONLY in the root `package.json` of the project doing the installing. A block declared by a
|
|
6
|
+
* DEPENDENCY is inert — it is not read, not merged, and not carried in the published tarball. So
|
|
7
|
+
* the monorepo root can (and does) pin these for itself while every consumer of the published
|
|
8
|
+
* packages still resolves the vulnerable versions. Repo green, install red, and nobody working in
|
|
9
|
+
* the repo ever sees what a consumer sees. Issue #764.
|
|
10
|
+
*
|
|
11
|
+
* Sungen already writes to the consuming project's `package.json` (standard scripts on `init`,
|
|
12
|
+
* family version pinning on `update`), so the overrides go in on the same two passes. That makes
|
|
13
|
+
* the fix reachable the ordinary way: a new project is clean from its first install, an existing
|
|
14
|
+
* one becomes clean on the next `sungen update`.
|
|
15
|
+
*
|
|
16
|
+
* Every entry below is here because NOTHING ELSE CLEARS IT — the offending package is either at its
|
|
17
|
+
* latest published version already, or is hard-pinned by an unmaintained parent. Bumping sungen's
|
|
18
|
+
* own declared ranges does not move any of them: `@sungen/driver-mobile` is already on the newest
|
|
19
|
+
* appium (3.x) and webdriverio (9.x), and npm's own `fix available` for that chain is a DOWNGRADE
|
|
20
|
+
* to appium 1.22.3 / webdriverio 8.14.6, not a fix.
|
|
21
|
+
*
|
|
22
|
+
* Measured on a clean-room install of the published 3.2.32 family (2026-09-23): 25 findings
|
|
23
|
+
* (12 moderate, 13 high) without this block, 0 with it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export interface SecurityOverride {
|
|
27
|
+
/** Package to pin, as it appears in `overrides`. */
|
|
28
|
+
pkg: string;
|
|
29
|
+
/** Range to pin it to — the lowest published version clear of the advisory. */
|
|
30
|
+
range: string;
|
|
31
|
+
/** Advisory + the path that drags the vulnerable copy in, so a reader can re-check the entry. */
|
|
32
|
+
why: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The pinned set. Keep it MINIMAL: an entry earns its place only while a clean install is red
|
|
37
|
+
* without it. When an upstream finally ships a fix inside its own declared range, delete the entry
|
|
38
|
+
* rather than raising it — a stale override silently holds a whole subtree back.
|
|
39
|
+
*/
|
|
40
|
+
export const SECURITY_OVERRIDES: SecurityOverride[] = [
|
|
41
|
+
{
|
|
42
|
+
pkg: 'uuid',
|
|
43
|
+
range: '^11.1.1',
|
|
44
|
+
// exceljs 4.4.0 IS the latest (published 2024-12-20) and hard-declares "uuid": "^8.3.0".
|
|
45
|
+
// There is no newer exceljs to bump to, which is why npm reports fixAvailable: false.
|
|
46
|
+
why: 'GHSA-w5hq-g745-h8pq (moderate) — uuid@8.3.2 via exceljs@4.4.0, which is unmaintained and pins ^8.3.0',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
pkg: 'morgan',
|
|
50
|
+
range: '^1.12.1',
|
|
51
|
+
// @appium/base-driver pins the exact version "1.11.0", so no resolution reaches 1.12.x on its own.
|
|
52
|
+
why: 'GHSA-jxfw-x594-9x9m (moderate) — morgan@1.11.0 exact-pinned by @appium/base-driver',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
pkg: 'serialize-javascript',
|
|
56
|
+
range: '^7.1.2',
|
|
57
|
+
why: 'GHSA-5c6j-r48x-rmvq (high) + GHSA-qj8w-gfj5-8c6v (moderate) — serialize-javascript@6.0.2 via mocha (^6.0.2)',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
pkg: '@puppeteer/browsers',
|
|
61
|
+
range: '^3.2.3',
|
|
62
|
+
// The vulnerable package is extract-zip, whose latest (2.0.1) IS the flagged version — there is
|
|
63
|
+
// nothing to pin it to. @puppeteer/browsers 3.x dropped extract-zip for modern-tar, so the pin
|
|
64
|
+
// goes on the parent instead. @wdio/utils asks for ^2.2.0 but imports only install, canDownload,
|
|
65
|
+
// resolveBuildId, detectBrowserPlatform, Browser, ChromeReleaseChannel and computeExecutablePath
|
|
66
|
+
// — all seven still exported by 3.2.3, verified against @wdio/utils@9.32.0.
|
|
67
|
+
why: 'GHSA-jmr9-qjv8-65gv + GHSA-7pqw-9j4j-h8q3 (high) — extract-zip@2.0.1 via @puppeteer/browsers@2.x ← @wdio/utils; extract-zip has no fixed release, 3.x replaced it with modern-tar',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { SECURITY_OVERRIDES, SecurityOverride } from './security-overrides-catalog';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merge the security-override catalog into a consuming project's `package.json`.
|
|
7
|
+
*
|
|
8
|
+
* The catalog itself — which packages, which ranges, and why each one cannot be cleared any other
|
|
9
|
+
* way — lives in `security-overrides-catalog.ts`. That is the file to edit when an advisory moves;
|
|
10
|
+
* this one holds only the merge, the write and the reporting, which do not change when it does.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { SECURITY_OVERRIDES } from './security-overrides-catalog';
|
|
14
|
+
export type { SecurityOverride } from './security-overrides-catalog';
|
|
15
|
+
|
|
16
|
+
export interface OverridePlan {
|
|
17
|
+
/** Entries missing from the project, to be written. */
|
|
18
|
+
add: SecurityOverride[];
|
|
19
|
+
/** Entries the project already pins itself — left untouched, reported so the user can judge. */
|
|
20
|
+
existing: Array<{ pkg: string; declared: string; wanted: string }>;
|
|
21
|
+
/** Set when the pins were needed but the write failed — `add` is then empty, nothing was applied. */
|
|
22
|
+
writeFailed?: boolean;
|
|
23
|
+
/** Lockfile of a package manager that ignores `overrides`, when the project has one. */
|
|
24
|
+
foreignLockfile?: 'yarn' | 'pnpm';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Decide what a project's `overrides` block is missing. Pure — no I/O, so the tests and `--dry-run`
|
|
29
|
+
* can plan without touching anything.
|
|
30
|
+
*
|
|
31
|
+
* A pin the project already declares is NEVER rewritten, even when it differs from ours. Someone who
|
|
32
|
+
* typed a range into their own `package.json` had a reason (a private mirror, a pinned fork, a
|
|
33
|
+
* downgrade forced by another tool), and silently overwriting it would be the more surprising bug.
|
|
34
|
+
* It is reported instead, so a still-vulnerable pin is visible rather than swallowed.
|
|
35
|
+
*/
|
|
36
|
+
export function planSecurityOverrides(pkgJson: Record<string, any>): OverridePlan {
|
|
37
|
+
const current: Record<string, unknown> =
|
|
38
|
+
pkgJson.overrides && typeof pkgJson.overrides === 'object' ? pkgJson.overrides : {};
|
|
39
|
+
|
|
40
|
+
const add: SecurityOverride[] = [];
|
|
41
|
+
const existing: OverridePlan['existing'] = [];
|
|
42
|
+
|
|
43
|
+
for (const entry of SECURITY_OVERRIDES) {
|
|
44
|
+
const declared = current[entry.pkg];
|
|
45
|
+
if (typeof declared === 'string') {
|
|
46
|
+
if (declared !== entry.range) existing.push({ pkg: entry.pkg, declared, wanted: entry.range });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// A nested override object ({ "uuid": { ".": "^11" } }) is a deliberate, more specific shape —
|
|
50
|
+
// treat it as the project's own decision and leave it alone.
|
|
51
|
+
if (declared !== undefined) {
|
|
52
|
+
existing.push({ pkg: entry.pkg, declared: JSON.stringify(declared), wanted: entry.range });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
add.push(entry);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { add, existing };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Write the missing overrides into a project's `package.json`.
|
|
63
|
+
*
|
|
64
|
+
* Best-effort by design, exactly like the rest of the project-file touching in `init` / `update`: an
|
|
65
|
+
* unreadable or malformed `package.json` means we skip silently rather than fail the command the
|
|
66
|
+
* user actually ran. The audit gate is not worth losing a scaffold over.
|
|
67
|
+
*
|
|
68
|
+
* Must run BEFORE the project's `npm install`, or the first resolution pass writes a lockfile that
|
|
69
|
+
* already pins the vulnerable versions.
|
|
70
|
+
*
|
|
71
|
+
* @param dryRun plan only — report what would change and write nothing
|
|
72
|
+
* @returns the plan that was applied, or null when there was no `package.json` to apply it to
|
|
73
|
+
*/
|
|
74
|
+
export function ensureSecurityOverrides(packageJsonPath: string, dryRun = false): OverridePlan | null {
|
|
75
|
+
let pkgJson: Record<string, any>;
|
|
76
|
+
try {
|
|
77
|
+
pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const plan = planSecurityOverrides(pkgJson);
|
|
83
|
+
plan.foreignLockfile = detectForeignLockfile(path.dirname(packageJsonPath));
|
|
84
|
+
|
|
85
|
+
if (plan.add.length && !dryRun) {
|
|
86
|
+
const overrides: Record<string, unknown> =
|
|
87
|
+
pkgJson.overrides && typeof pkgJson.overrides === 'object' ? pkgJson.overrides : {};
|
|
88
|
+
for (const entry of plan.add) overrides[entry.pkg] = entry.range;
|
|
89
|
+
pkgJson.overrides = overrides;
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf-8');
|
|
92
|
+
} catch {
|
|
93
|
+
// Read-only checkout, root-owned file, read-only container mount. `sungen update` in
|
|
94
|
+
// particular has ALREADY reinstalled globally by the time it gets here, so throwing would
|
|
95
|
+
// abandon the AI-rules refresh and the family sync over a hardening step. Report and move on.
|
|
96
|
+
return { add: [], existing: plan.existing, foreignLockfile: plan.foreignLockfile, writeFailed: true };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return plan;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* npm is the only package manager that reads a top-level `overrides` block. Yarn wants
|
|
105
|
+
* `resolutions`, pnpm wants `pnpm.overrides` — so on those projects the block we write is inert and
|
|
106
|
+
* the install stays vulnerable. We do NOT guess at the equivalent field (getting a resolutions
|
|
107
|
+
* pattern subtly wrong is worse than not writing one), but silence would be the same class of bug
|
|
108
|
+
* as #764 itself: a fix that looks applied and is not. So: say so.
|
|
109
|
+
*/
|
|
110
|
+
function detectForeignLockfile(dir: string): 'yarn' | 'pnpm' | undefined {
|
|
111
|
+
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
112
|
+
if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Print what `ensureSecurityOverrides` did, in the voice the surrounding commands already use. */
|
|
117
|
+
export function reportSecurityOverrides(plan: OverridePlan | null, dryRun = false): void {
|
|
118
|
+
if (!plan) return;
|
|
119
|
+
|
|
120
|
+
if (plan.writeFailed) {
|
|
121
|
+
console.warn(
|
|
122
|
+
'⚠️ Could not write the security overrides into package.json (read-only file?). Nothing else\n' +
|
|
123
|
+
' failed. Add this block by hand to keep `npm audit` clean:\n' +
|
|
124
|
+
` "overrides": ${JSON.stringify(Object.fromEntries(SECURITY_OVERRIDES.map((o) => [o.pkg, o.range])), null, 2).replace(/\n/g, '\n ')}\n`,
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (plan.add.length) {
|
|
130
|
+
const verb = dryRun ? '[dry-run] would add' : 'Added';
|
|
131
|
+
console.log(`🔒 ${verb} ${plan.add.length} security override(s) to package.json — npm honours these only here, not from sungen's own manifest:`);
|
|
132
|
+
for (const entry of plan.add) console.log(` • ${entry.pkg}@${entry.range} — ${entry.why}`);
|
|
133
|
+
console.log('');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (plan.existing.length) {
|
|
137
|
+
console.log('🔒 Left your own overrides alone (sungen never rewrites a pin you declared):');
|
|
138
|
+
for (const e of plan.existing) console.log(` – ${e.pkg}: ${e.declared} (sungen would use ${e.wanted})`);
|
|
139
|
+
console.log(' If `npm audit` still flags one of these, raise it by hand.\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (plan.foreignLockfile && (plan.add.length || plan.existing.length)) {
|
|
143
|
+
const field = plan.foreignLockfile === 'pnpm' ? '"pnpm": { "overrides": … }' : '"resolutions"';
|
|
144
|
+
console.warn(
|
|
145
|
+
`⚠️ This project has a ${plan.foreignLockfile} lockfile. ${plan.foreignLockfile} ignores npm's "overrides" —\n` +
|
|
146
|
+
` mirror the block into ${field} as well, or the pins do nothing here.\n`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -93,6 +93,20 @@ every variant is N/A) and never a bare state word. **`N/A` leaves the denominato
|
|
|
93
93
|
and actual result share one column `P`; the header row is frozen; ISO dates; landscape print with
|
|
94
94
|
repeated header rows.
|
|
95
95
|
|
|
96
|
+
**Re-running delivery keeps the tester's Manual results** (#756, since 3.2.33). Before overwriting,
|
|
97
|
+
the CLI reads the existing workbook's **Manual** sheet and carries each variant's `L`–`P` cells into
|
|
98
|
+
the new one by `VP-…` ref, verbatim. A hidden `_sungen_manual` sheet stamps each case's fingerprint,
|
|
99
|
+
so a result recorded against an older procedure is reset to `Pending` with the previous result
|
|
100
|
+
moved into Note. Relay the CLI's `Manual results:` lines to the user: `reset to Pending` means those
|
|
101
|
+
cases must be run again by hand; `not carried` names results whose case left the feature. The
|
|
102
|
+
**Auto** sheet is not carried (the run report owns its Result), and rows a tester added by hand
|
|
103
|
+
have no ref and are not carried. Never tell the user to copy or rename the file to protect manual
|
|
104
|
+
results. A second `not carried` line names a duplicated row (only the first copy is kept) — tell
|
|
105
|
+
the user to merge it by hand. If the CLI prints `not written: cannot read …` (locked or damaged) or
|
|
106
|
+
`not written: … has no "Manual" sheet` (renamed), the unit was deliberately NOT written to protect
|
|
107
|
+
the results: ask the user to close the file or rename the sheet back to `Manual`, or move the file
|
|
108
|
+
aside. Never delete, rename or move it yourself.
|
|
109
|
+
|
|
96
110
|
`Coverage` sheet — requirement coverage table (every FR/TR/NFR id with an explicit status),
|
|
97
111
|
target × category grid with explicit `—` gaps, dispositions, manifest. CSV mirrors the same model
|
|
98
112
|
flat with a `Level` column (`item`/`variant`) + a requirement-coverage appendix.
|
|
@@ -128,8 +128,10 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
|
|
|
128
128
|
| `SG-W034` | A `see [X] column with {{v}}` step runs inside a table whose rows are declared with a **`rows:` shape**, so the row is not an ARIA row and holds no ARIA cells — but the assertion's only other way to reach a cell is `getByRole('cell')`, which matches nothing there. `columns.<col>.index` cannot rescue it: it indexes that same empty set. Before this the step failed as a timeout blaming the app. Not reported on a real `<table>`, where the role fallback is correct | Name the cell on the column: `columns: { title: { index: 0, header: 'Title', elements: { cell: { type: locator, value: '.job-title' } } } }`. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
|
|
129
129
|
| `SG-E035` | **`table match data:` on a table declared with a `rows:` shape** and no matching `columns:` entries. The assertion reads its header row from `thead tr` and addresses cells as `td, th`; a <div> list has neither — forever, not as a render race — so the step could only wait out every timeout and fail on an empty header row. Refused at compile time, naming the columns it needs | Declare each DataTable column on the table entry with the selector that matches its cell inside a row: `columns: { title: { index: 0, header: 'Title', elements: { cell: { type: locator, value: '.job-title' } } } }`. Declaring columns also removes the runtime header read on a real `<table>`. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
|
|
130
130
|
| `SG-W036` | A **`remember [X] … as {{v}}` capture through a `locator`/`css` selector with no `nth:`**. A capture reads ONE element and the step takes the FIRST match — the right default, and what the documented `nth: 0` workaround asked for after Playwright's strict mode turned a list-shaped selector into a hard failure naming the count. But the first of many is a decision: if the selector describes every item in a list, the captured value is the first row's. Not reported for testid/label/placeholder/role+name/text entries, which name a particular thing | Declare the index (`nth: 0`) to say the first one is meant, or narrow the selector to the single element. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
|
|
131
|
+
| `SG-W037` | A **`type: page` entry saying something about the QUERY that the compiler cannot use**. Two causes: the entry's `value` carries a `?`/`#`, which is DROPPED (a page value is the path only — the pattern is matched against `u.pathname`, which never holds a query), or `query:` declares something other than `any`, which is ignored rather than read as permissive so a typo cannot quietly switch off the bare-page assertion. Both used to be silent, and the first is the trap: an author who hit "a bare `see [X] page` requires NO query" reaches for the query in the value, gets byte-identical output, and no hint why | To assert the query, declare it on the STEP (`see [X] page with {{v}}`). If the query is generated at run time — `/admins/detail?id=<the row just clicked>`, which no literal can express — add `query: any` to the entry: the query is then not part of that page's identity. selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
|
|
132
|
+
| `SG-E038` | A **`{{var}}` the compiler cannot resolve** — the feature has no test-data file of its own, the key is absent, or it holds a list/map where a value was needed. This used to emit the PLACEHOLDER TEXT as the value: `fill [Partner Customer ID] field with {{valid_partner_id}}` compiled to `fill('${valid_partner_id}')`, typing those 20 literal characters into the app with `generate` exiting 0. A field with no format validation accepts the string, the server sees an unknown user, and the scenario fails at the assertion it was written to fail at — a suite that looks correct while feeding the system garbage. Every run also shares the SAME literal, so a scenario needing a fresh user silently reuses one. The step now compiles to a `throw` naming the ref and the file | Add the key to the file the message names. Note a feature resolves `{{var}}` against a data file named after **itself** — `qa/screens/<screen>/test-data/<feature>.yaml` — so the second `.feature` in a screen directory needs its own data file, not the screen-named one. `sungen data crosscheck` reports the same gap before you compile |
|
|
131
133
|
|
|
132
|
-
A diagnostic never changes generation's own exit code — `sungen generate --strict` turns any collected diagnostic into a non-zero exit for CI gates. They are not all inert, though: **`SG-W001`, `SG-W011`, `SG-E020`, `SG-E021`, `SG-W020`, `SG-E023`, `SG-E024`, `SG-E025`, `SG-E026`, `SG-E027` and `SG-
|
|
134
|
+
A diagnostic never changes generation's own exit code — `sungen generate --strict` turns any collected diagnostic into a non-zero exit for CI gates. They are not all inert, though: **`SG-W001`, `SG-W011`, `SG-E020`, `SG-E021`, `SG-W020`, `SG-E023`, `SG-E024`, `SG-E025`, `SG-E026`, `SG-E027`, `SG-E035` and `SG-E038` additionally compile the offending step to a runtime `throw`, and `SG-E031` refuses the whole file** — a step that reads as a check and silently checks nothing is worse than one that fails loudly. Generation always COMPLETES: the rest of the scenario and the rest of the file still compile. `SG-E022` is the odd one out — it blames the compiler, not your `.feature`, and throws nothing (the file is written so you can inspect it).
|
|
133
135
|
|
|
134
136
|
### Runtime error → `Test data "<key>" references ${QA_*} but the environment variable is not set`
|
|
135
137
|
|
|
@@ -630,6 +630,7 @@ on mobile a dialog-scoped ref still resolves against the whole screen (`SG-W021`
|
|
|
630
630
|
|---|---|
|
|
631
631
|
| `@spec:<id>` | **Traceability**: this scenario proves requirement `<id>` from `requirements/spec.md` (`@spec:FR-003`). Repeatable — a scenario may prove several. Read by `sungen audit` (requirement coverage) and by delivery (the Coverage sheet's requirement table). Without it the link is only inferred from wording and cannot survive an edit. |
|
|
632
632
|
| `@manual` | Skip in generation |
|
|
633
|
+
| `@known-defect` | The scenario is EXPECTED to be red — it asserts a live defect. It still runs and still reports as failed (never skipped, never "fixed"), and it stays a pass-through tag so `--grep @known-defect` selects it. **`[mobile]`** Its failure does NOT escalate the next scenario's `@reset:` tier: a red that was designed in is not evidence of leftover state |
|
|
633
634
|
| `@auth:role` | Use auth storage state for role |
|
|
634
635
|
| `@no-auth` | Disable inherited auth |
|
|
635
636
|
| `@steps:name` | Define reusable step block (base scenario) |
|
|
@@ -695,6 +696,14 @@ Rules that matter when generating:
|
|
|
695
696
|
relaunch does not undo a half-written local DB. One rung, one scenario, and never from `none`.
|
|
696
697
|
A **retry** of the failed scenario is NOT escalated — it re-runs at its declared tier, so a
|
|
697
698
|
state-dependent failure fails both attempts and is reported honestly instead of passing on attempt 2.
|
|
699
|
+
A scenario tagged **`@known-defect`** escalates nothing either: its red was designed in, so making
|
|
700
|
+
the next scenario pay a harder reset for it buys nothing, on every run, forever.
|
|
701
|
+
- **Escalation never picks a tier this session cannot perform.** Reinstalling needs the app's build
|
|
702
|
+
file, so when the app is attached by package the ladder caps at `data` (Android) / `soft` (iOS) and
|
|
703
|
+
says it capped, instead of throwing. Nothing asked for the escalated tier, and the throw would land
|
|
704
|
+
in `beforeEach` — a hook failure, which skips every remaining scenario in the FILE. The cap never
|
|
705
|
+
goes BELOW the declared tier: a tier you asked for that the session cannot do still throws by name,
|
|
706
|
+
because that one is an unmet contract rather than a recovery the runner chose.
|
|
698
707
|
- **`SUNGEN_RESET_TIER=data` raises the floor for a whole run** without regenerating specs — CI can run
|
|
699
708
|
stricter than local dev. It never lowers a declared tier.
|
|
700
709
|
- **`data`/`full` on iOS reinstall the app**, because `mobile: clearApp` is Android-only (UiAutomator2).
|
|
@@ -128,7 +128,10 @@ back to the feature's own path (or `/<ref>/`) and warns `SG-W013`: a test that r
|
|
|
128
128
|
|
|
129
129
|
If the label is used by only ONE type, the base key (no suffix) is fine — suffix only on collision.
|
|
130
130
|
|
|
131
|
-
Type aliases (normalized automatically)
|
|
131
|
+
Type aliases (normalized automatically). The suffix and the step may use **different spellings of
|
|
132
|
+
the same type** — `confirm--modal` resolves for `[Confirm] dialog` and `confirm--dialog` resolves
|
|
133
|
+
for `[Confirm] modal`. Prefer the normalized suffix for consistency; either is found.
|
|
134
|
+
|
|
132
135
|
| Gherkin type | Normalized suffix |
|
|
133
136
|
|---|---|
|
|
134
137
|
| title, label, caption, message | `text` |
|
|
@@ -143,6 +146,15 @@ Type aliases (normalized automatically):
|
|
|
143
146
|
| column | `columnheader` |
|
|
144
147
|
| list-item | `listitem` |
|
|
145
148
|
|
|
149
|
+
When the step names a type from this table, only a key whose suffix is a spelling of that type is
|
|
150
|
+
used — `[Confirm] dialog` will not fall back to an unrelated `confirm--popup` entry. It reports the
|
|
151
|
+
keys it looked for, so the fix is to add one of them.
|
|
152
|
+
|
|
153
|
+
A bare `[Confirm]` keeps the older, looser fallback and matches any `confirm--*` entry, because with
|
|
154
|
+
no type there is nothing to match a suffix against. So does a `[Ref]` followed by a word that is not
|
|
155
|
+
an element type at all — `Then User see [Quick Panel] is hidden` reads `is` as that word. This is
|
|
156
|
+
what keeps state-clause steps working on a label whose keys are all suffixed.
|
|
157
|
+
|
|
146
158
|
## Nth-Suffixed Keys
|
|
147
159
|
|
|
148
160
|
When targeting a specific occurrence by index, add `--N` suffix:
|
|
@@ -42,18 +42,30 @@ interface LocaleInjection {
|
|
|
42
42
|
|
|
43
43
|
let cached: LocaleInjection | null | undefined;
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Absent config → null, and locale injection is simply off (the documented no-op).
|
|
47
|
+
*
|
|
48
|
+
* A config that EXISTS but does not parse is a different thing and must not read the same way: the
|
|
49
|
+
* header above tells you to hand-edit this file, and a trailing comma would otherwise turn a
|
|
50
|
+
* `SUNGEN_ENV=ja` run into a base-locale run that asserts base-locale strings — green, and wrong.
|
|
51
|
+
* We warn rather than throw, because the injection is genuinely optional and failing every test on
|
|
52
|
+
* it would be a larger change than the fault warrants; the line names the file and the consequence.
|
|
53
|
+
*/
|
|
45
54
|
function loadLocaleConfig(): LocaleInjection | null {
|
|
46
55
|
if (cached !== undefined) return cached;
|
|
56
|
+
const cfgPath = path.join(__dirname, 'locale-config.json');
|
|
57
|
+
if (!fs.existsSync(cfgPath)) {
|
|
58
|
+
cached = null;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
47
61
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return parsed;
|
|
56
|
-
} catch {
|
|
62
|
+
cached = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) as LocaleInjection;
|
|
63
|
+
return cached;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error(
|
|
66
|
+
`⚠️ ${cfgPath}: unreadable (${e instanceof Error ? e.message.split('\n')[0] : String(e)}) — ` +
|
|
67
|
+
`locale state is NOT injected, so this SUNGEN_ENV run behaves as the base locale.`,
|
|
68
|
+
);
|
|
57
69
|
cached = null;
|
|
58
70
|
return null;
|
|
59
71
|
}
|
|
Binary file
|
|
@@ -46,6 +46,16 @@ export interface UrlExpectation {
|
|
|
46
46
|
declared?: string;
|
|
47
47
|
/** The test-data key behind `declared`, so a failure can name what an author must go fix. */
|
|
48
48
|
dataRef?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The page selector declared `query: any` — the query string is not part of this page's identity,
|
|
51
|
+
* so SILENCE from the step stops meaning "there must be no query" (#748).
|
|
52
|
+
*
|
|
53
|
+
* Emitted only for a page whose entry says so. The default stays strict: a bare page means a bare
|
|
54
|
+
* page, which is the #464 assertion and the reason a filter regression cannot read as green.
|
|
55
|
+
* It is the no-`dataRef` branch alone that this relaxes — a step that declares `with {{v}}` still
|
|
56
|
+
* asserts every param it names, on this page as on any other.
|
|
57
|
+
*/
|
|
58
|
+
ignoreQuery?: boolean;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
/**
|
|
@@ -113,8 +123,14 @@ function locationMatches(pathname: RegExp, hostname: RegExp | undefined, u: URL)
|
|
|
113
123
|
* tracking/UTM param the test never cared about cannot turn it red.
|
|
114
124
|
* A declared fragment is asserted too; a declared value carrying no query
|
|
115
125
|
* at all constrains the path only (the compiler warns: SG-W014).
|
|
126
|
+
*
|
|
127
|
+
* `query: any` on the page's selector entry overrides the FIRST rule only (#748): silence then
|
|
128
|
+
* means "the query is not mine to judge" instead of "there must be none". It is the escape hatch
|
|
129
|
+
* for a page whose query is generated at run time — `/admins/detail?id=<the row just clicked>` —
|
|
130
|
+
* which the strict rule makes unassertable and `with {{v}}` cannot express, since that asserts a
|
|
131
|
+
* literal. The second rule is untouched: declare params and they are still all checked.
|
|
116
132
|
*/
|
|
117
|
-
export function urlMatches({ pathname, hostname, declared, dataRef }: UrlExpectation): (u: URL) => boolean {
|
|
133
|
+
export function urlMatches({ pathname, hostname, declared, dataRef, ignoreQuery }: UrlExpectation): (u: URL) => boolean {
|
|
118
134
|
return (u: URL): boolean => {
|
|
119
135
|
if (!locationMatches(pathname, hostname, u)) return false;
|
|
120
136
|
|
|
@@ -123,7 +139,13 @@ export function urlMatches({ pathname, hostname, declared, dataRef }: UrlExpecta
|
|
|
123
139
|
// undefined, and they mean opposite things: the first is a real assertion (a bare page must be
|
|
124
140
|
// bare), the second is broken test-data. Only the generator knows which, and it says so by
|
|
125
141
|
// emitting `dataRef` exactly when the step carried a `with {{v}}`.
|
|
126
|
-
|
|
142
|
+
// `query: any` on the page entry (#748): the query is not part of this page's identity, so
|
|
143
|
+
// silence from the step is not an assertion about it. Without this there is NO way to express
|
|
144
|
+
// "this path, whatever the query" — `with {{v}}` asserts a LITERAL value, and a detail page
|
|
145
|
+
// keyed by `?id=<the row just clicked>` has no literal to declare. Checked before the bare-page
|
|
146
|
+
// rule, never instead of the declared-value rule below: a step that names params still asserts
|
|
147
|
+
// them here exactly as anywhere else.
|
|
148
|
+
if (!dataRef) return ignoreQuery ? true : u.search === '';
|
|
127
149
|
|
|
128
150
|
// Silence is the whole bug class this assertion exists to kill, so an unusable declared value
|
|
129
151
|
// is an ERROR, not a quiet verdict. Returning false would read as "the app is wrong"; these
|
|
@@ -48,6 +48,14 @@ function evidenceSlug(s: string): string {
|
|
|
48
48
|
.slice(0, 80) || 'unnamed';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* True only in the launcher process. `@wdio/runner` re-parses this very config inside EVERY worker
|
|
53
|
+
* it spawns, so anything logged at module scope prints once per spec unless it is gated. WDIO's own
|
|
54
|
+
* "am I a worker" test is this env var (`@wdio/local-runner` sets it when spawning; `@wdio/utils`
|
|
55
|
+
* reads it) — reuse it rather than inventing a second signal.
|
|
56
|
+
*/
|
|
57
|
+
const IS_LAUNCHER = !process.env.WDIO_WORKER_ID;
|
|
58
|
+
|
|
51
59
|
/**
|
|
52
60
|
* Auto-select MOBILE specs and classify each by its target OS (dual-target routing).
|
|
53
61
|
*
|
|
@@ -82,7 +90,12 @@ function classifyMobileSpecs(root: string): Array<{ file: string; platform: stri
|
|
|
82
90
|
.filter((full) => {
|
|
83
91
|
try {
|
|
84
92
|
return fs.readFileSync(full, 'utf-8').includes('@wdio/globals');
|
|
85
|
-
} catch {
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// A spec listed by readdir that cannot be read cannot be classified, so it is excluded.
|
|
95
|
+
// Excluding it SILENTLY is the fault: WDIO only fails the run when the spec list is
|
|
96
|
+
// EMPTY ("No specs found to run"), so dropping one spec out of ten leaves a green suite
|
|
97
|
+
// containing a test that never executed. Name it, and let the count below show the shortfall.
|
|
98
|
+
if (IS_LAUNCHER) console.warn(`[sungen] could not read ${full} — EXCLUDED from this run: ${(e as Error).message}`);
|
|
86
99
|
return false;
|
|
87
100
|
}
|
|
88
101
|
})
|
|
@@ -98,6 +111,16 @@ const androidSpecs = specEntries.filter((e) => e.platform !== 'ios').map((e) =>
|
|
|
98
111
|
const iosSpecs = specEntries.filter((e) => e.platform !== 'android').map((e) => e.file);
|
|
99
112
|
const mobileSpecs = specEntries.map((e) => e.file); // union — top-level fallback
|
|
100
113
|
|
|
114
|
+
// What DISCOVERY found — a mobile suite is only trustworthy if you can see how many specs it
|
|
115
|
+
// resolved: WDIO fails loudly on an EMPTY list but says nothing when the list is merely short,
|
|
116
|
+
// which is exactly what a dropped spec looks like.
|
|
117
|
+
//
|
|
118
|
+
// Two things this number is not. It is not an error on 0: WDIO already exits 1 there
|
|
119
|
+
// ("No specs found to run"), and a `--shard` that legitimately receives none must still return 0.
|
|
120
|
+
// And under `--shard` it is the PRE-SHARD total, because ConfigParser slices `specs` down to the
|
|
121
|
+
// shard's portion only after this file has finished evaluating.
|
|
122
|
+
if (IS_LAUNCHER) console.log(`[sungen] specs: ${specEntries.length} mobile spec(s) — android ${androidSpecs.length}, ios ${iosSpecs.length}`);
|
|
123
|
+
|
|
101
124
|
// App under test — env overrides the init-configured defaults.
|
|
102
125
|
const APP_PACKAGE = process.env.APP_PACKAGE || '__APP_PACKAGE__';
|
|
103
126
|
const APP_ACTIVITY = process.env.APP_ACTIVITY || '__APP_ACTIVITY__';
|